3 ###----- Licensing notice ---------------------------------------------------
5 ### This program is free software: you can redistribute it and/or modify it
6 ### under the terms of the GNU General Public License as published by
7 ### the Free Software Foundation; either version 2 of the License, or (at
8 ### your option) any later version.
10 ### This program is distributed in the hope that it will be useful, but
11 ### WITHOUT ANY WARRANTY; without even the implied warranty of
12 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 ### Public License for more details.
15 ### You should have received a copy of the GNU General License along with
16 ### mLib. If not, write to the Free Software Foundation, Inc., 59 Temple
17 ### Place - Suite 330, Boston, MA 02111-1307, USA.
19 ###--------------------------------------------------------------------------
32 ###--------------------------------------------------------------------------
33 ### Other preliminaries.
35 (our $PROG = $0) =~ s!^.*/!!;
36 our $VERSION = '@VERSION@';
38 ## Check arguments. This is easy: there aren't any.
39 sub usage (*) { my ($f) = @_; print $f "usage: $PROG\n"; }
43 elsif ($arg eq "-h" || $arg eq "--help")
44 { usage STDOUT; exit 0; }
45 elsif ($arg eq "-v" || $arg eq "--version")
46 { print "$PROG, version $VERSION\n"; exit 0; }
50 sub maybe_mkdir ($;$) {
51 my ($dir, $mode) = @_;
52 ## Create a directory DIR with permissions MODE, defaulting to 0777, before
53 ## umask. Ignore errors complaining that the directory already exists.
55 eval { mkdir $dir, $mode // 0777; }; die if $@ && $@->errno != EEXIST;
58 sub maybe_unlink ($) {
60 ## Delete FILE. Ignore errors complaining that the FILE doesn't exist.
62 eval { unlink $file; }; die if $@ and $@->errno != ENOENT;
65 sub present_time ($) {
67 ## Format T as a human-readable time.
69 return strftime "%Y-%m-%d %H:%M:%S %z", localtime $t;
73 my ($fout, $file) = @_;
74 ## Copy the contents of FILE to the stream FOUT.
76 open my $fin, "<", $file;
78 while (sysread $fin, $buf, 65536) { print $fout $buf; }
84 ## Parse an ISO8601 zulu-time stamp to POSIX `time_t'.
86 my ($y, $mo, $d, $h, $mi, $s) =
87 $stamp =~ m{^ (\d{4}) - (\d{2}) - (\d{2}) T
88 (\d{2}) : (\d{2}) : (\d{2}) Z $}x
89 or die "bad timestamp `$stamp'";
90 return timegm $s, $mi, $h, $d, $mo - 1, $y - 1900;
93 sub present_stamp ($) {
95 ## Format a POSIX `time_t' as a string.
97 my ($s, $mi, $h, $d, $mo, $y, $wd, $yd, $dstp) = gmtime $t;
98 return sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
99 $y + 1900, $mo + 1, $d, $h, $mi, $s;
103 my ($body, @prog) = @_;
104 ## Run the BODY, printing a banner above and below, including a proposed
105 ## program name and arguments PROG, and the exit status.
107 my $label = sprintf "RUN %s ", join " ", @prog;
108 print $label . ">" x (77 - length $label) . "\n";
110 if (wantarray) { @r = $body->(); }
111 else { @r = scalar $body->(); }
112 print "<" x 77 . "\n";
114 elsif ($?%256) { printf "command killed by signal %d\n", $?%256; }
115 else { printf "command exited with status %d\n", int($?/256); }
116 die "command failed" if $?;
124 running { system @prog; } @prog;
127 ## Set the current time.
129 if (!defined $ENV{"DKIM_KEYS_TIMENOW"}) {
132 $NOW = parse_stamp $ENV{"DKIM_KEYS_TIMENOW"};
133 printf "pretending time is %s\n", present_time $NOW;
136 printf "%s BEGINS at %s\n", $PROG, present_time $NOW;
138 ###--------------------------------------------------------------------------
142 sub C_UNV () { return 0; } # universal
143 sub C_APP () { return 1; } # application
144 sub C_CTX () { return 2; } # contextual
145 sub C_PRV () { return 3; } # private
147 ## BER universal tag numbers.
148 sub TY_INT () { return 2; } # INTEGER
149 sub TY_BITSTR () { return 3; } # BIT STRING
150 sub TY_OCTSTR () { return 4; } # OCTET STRING
151 sub TY_NULL () { return 5; } # NULL
152 sub TY_OID () { return 6; } # OBJECT IDENTIFIER
153 sub TY_SEQ () { return 16; } # SEQUENCE
154 sub TY_SET () { return 17; } # SET
158 ## Decode an encoded OID body. Return the OID in text format, as a list
159 ## of integers separated by `.'.
161 my ($h, @d) = unpack "C w*", $oid;
162 if ($h >= 0x80) { die "malformed BER (invalid OID)"; }
163 return join ".", int($h/40), $h%40, @d;
166 sub ber_decnext (\$) {
167 my ($str_inout) = @_;
168 ## Decode the next value from a BER-encoded string, updating STR_INOUT to
169 ## hold the remaining material. Returns four items (C, K, T, X), where C
170 ## is the tag class; K is a flag which is truish if the encoding is
171 ## constructed or falsish if primitive; T is the tag number; and X is the
174 my ($h0, $r) = unpack "C a*", $$str_inout;
175 my ($c, $k, $t) = (($h0 >> 6)&0x03, ($h0 >> 5)&0x01, ($h0 >> 0)&0x1f);
176 if ($t == 0x1f) { ($t, $r) = unpack "w a*", $r; }
178 (my $n, $r) = unpack "C a*", $r;
179 if ($n == 0x80) { die "indefinite encodings not supported"; }
180 elsif ($n == 0xff) { die "malformed BER (invalid length)"; }
183 my @n = unpack +(sprintf "C%d", $n), $r;
185 $n = 0; for my $i (@n) { $n = 256*$n + $i; }
187 $$str_inout = substr $r, $n; return ($c, $k, $t, substr $r, 0, $n);
191 my ($file, $label) = @_;
192 ## Read a PEM-encoded message from FILE, with LABEL quoted in the boundary
193 ## markers, and return the binary contents.
196 open my $f, "<", $file;
198 { last LINE if /^-----BEGIN \Q$label\E-----$/; }
200 { last LINE if /^-----END \Q$label\E-----$/; $body .= $_; }
203 return decode_base64 $body;
208 ## Parse the (raw BER) RSA public key data PUB, returning a key identifier.
209 ## This is the least significant 80 bits of the modulus, Base32-encoded,
210 ## which comes out at 16 characters.
215 ($c, $k, $t, my $x) = ber_decnext $pub;
216 $c == C_UNV && $k && $t == TY_SEQ
217 or die "malformed key (expected seq)";
220 ($c, $k, $t, my $y) = ber_decnext $x;
221 $c == C_UNV && $k && $t == TY_SEQ
222 or die "malformed key (expected seq)";
224 ## Key type OID; must be `rsaEncryption'.
225 ($c, $k, $t, my $o) = ber_decnext $y;
226 $c == C_UNV && !$k && $t == TY_OID
227 or die "malformed key (expected oid)";
228 ber_decoid($o) eq "1.2.840.113549.1.1.1"
229 or die "malformed key (wrong oid)";
231 ## Parameters; must be NULL.
232 ($c, $k, $t, my $z) = ber_decnext $y;
233 $c == C_UNV && !$k && $t == TY_NULL && $z eq ""
234 or die "malformed key (expected null)";
236 ## End inner SEQUENCE.
237 $y eq "" or die "malformed key (trailing junk)";
239 ## BIT STRING holding the actual public key data. I have no idea what
240 ## they were thinking when they came up with this.
241 ($c, $k, $t, my $u) = ber_decnext $x;
242 $c == C_UNV && !$k && $t == TY_BITSTR
243 or die "malformed key (expected bitstr)";
244 (my $n, $u) = unpack "C a*", $u;
245 $n == 0 or die "malformed key (odd-length bitstr)";
248 ($c, $k, $t, $y) = ber_decnext $u;
249 $c == C_UNV && $k && $t == TY_SEQ
250 or die "malformed key (expected seq)";
252 ## INTEGER holding the modulus.
253 ($c, $k, $t, $n) = ber_decnext $y;
254 $c == C_UNV && !$k && $t == TY_INT
255 or die "malformed key (expected int)";
257 ## INTEGER holding the public exponent, which we don't care about.
258 ($c, $k, $t, my $e) = ber_decnext $y;
259 $c == C_UNV && !$k && $t == TY_INT
260 or die "malformed key (expected int)";
262 ## Close the inner SEQUENCE, BIT STRING, outer SEQUENCE, and the
264 $y eq "" or die "malformed key (trailing junk)";
265 $u eq "" or die "malformed key (trailing junk)";
266 $x eq "" or die "malformed key (trailing junk)";
267 $pub eq "" or die "malformed key (trailing junk)";
269 ## Extract the low bits of the modulus and encode them.
270 length $n >= 10 or die "malformed key (too small)";
271 return lc encode_base32 substr($n, -10), "";
274 ###--------------------------------------------------------------------------
277 ## Conversion functions take two arguments, PARSEP and VAL. If PARSEP is
278 ## truish, then VAL is a value read from the configuration file, and the
279 ## function should parse it and return the Perl internal value. If PARSE is
280 ## falsish, then do the reverse conversion.
282 sub conf_token ($$) {
283 my ($parsep, $val) = @_;
284 ## Do-nothing conversion function for text.
289 sub conf_stamp ($$) {
290 my ($parsep, $val) = @_;
291 ## Convert ISO8601 zulu-time stamps to POSIX `time_t'.
293 if ($parsep) { return parse_stamp $val; }
294 else { return present_stamp $val; }
298 ## Map between unit names and scales.
299 my %unitmap; my @unitmap;
301 for my $item ([7*24*60*60, qw{wk w wks week weeks}],
302 [24*60*60, qw{dy d dys day days}],
303 [60*60, qw{hr h hrs hour hours}],
304 [60, qw{min m mins minute minutes}],
305 [1, "", qw{s sec secs second seconds}]) {
306 my ($scale, @names) = @$item;
307 for my $name (@names) { $unitmap{$name} = $scale; }
308 push @unitmap, [$scale, $names[0]];
311 sub conf_duration ($$) {
312 my ($parsep, $val) = @_;
313 ## Convert time durations with units.
316 my ($x, $u) = $val =~ m{^ \s* (\.\d+ | \d+ (?: \. \d*)?)
318 or die "bad duration `$val'";
319 my $scale = $unitmap{$u} or die "bad duration `$val'";
322 my $x = 1; my $u = "s";
323 UNIT: for my $item (@unitmap) {
324 my ($scale, $unit) = @$item;
325 if ($val >= $scale) { $x = $val/$scale; $u = $unit; last UNIT; }
327 return sprintf "%.4g %s", $x, $u;
332 sub read_config ($$) {
333 my ($spec, $file) = @_;
334 ## Read configuration from FILE, as described by SPEC. Return a hash
335 ## mapping names to values.
337 ## The SPEC is a hashref mapping configuration keys to [CONV, DFLT] pairs,
338 ## where CONV is a conversion function (as described above) and DFLT is the
342 open my $f, "<", $file;
344 chomp; next LINE unless /^\s*[^#]/;
345 my ($k, $v) = m{^ \s* ([^=\s]+) \s* = (.*) $}x
346 or die "bad assignment `$_'";
347 $v =~ s/^\s+//; $v =~ s/\s+$//;
348 my $def = $spec->{$k} or die "unknown setting `$k'";
349 exists $c{$k} and die "duplicate setting `$k'";
350 $c{$k} = $def->[0](1, $v);
353 for my $k (keys %$spec) {
354 if (exists $c{$k}) { }
355 elsif ($spec->{$k}->@* > 1) { $c{$k} = $spec->{$k}[1]; }
356 else { die "missing setting for `$k'"; }
361 sub write_config ($$$) {
362 my ($spec, $file, $conf) = @_;
363 ## Write configuration back FILE from CONF, as described by SPEC. See
364 ## `read_config' for the format of SPEC.
366 open my $f, ">", "$file.new";
367 KEY: for my $k (keys %$spec) {
368 defined $conf->{$k} or next KEY;
369 printf $f "%s = %s\n", $k, $spec->{$k}[0](0, $conf->{$k});
372 rename "$file.new", $file;
375 ###--------------------------------------------------------------------------
378 my %CONF_SPEC = ("ddns-zone" => [\&conf_token],
379 "ddns-server" => [\&conf_token, undef],
380 "ddns-key" => [\&conf_token],
381 "ddns-ttl" => [\&conf_duration, 4*60*60],
382 "dns-delay" => [\&conf_duration, 2*24*60*60],
383 "instance" => [\&conf_token],
384 "publish-uri" => [\&conf_token],
385 "active-duration" => [\&conf_duration, 24*60*60],
386 "cycle-period" => [\&conf_duration, 3*24*60*60],
387 "mail-persistence" => [\&conf_duration, 7*24*60*60],
388 "dns-persistence" => [\&conf_duration, 3*24*60*60]);
390 our %C = %{read_config \%CONF_SPEC, "dkim-keys.conf"};
392 ###--------------------------------------------------------------------------
395 ## There are six states that a key can be in. Keys always advance through
396 ## these states in order. Except in `NEW' and `PUBLISH', the key's
397 ## private-key file is reliably named `ST/TS.ID.priv', and this is the
398 ## primary means to determine which state the key is actually in.
400 ## * `NEW'. This is rather complicated, and there are multiple steps and
401 ## a fiddly cleanup procedure.
403 ## 1. Create `NEW/new.priv'. Cleanup: delete it.
404 ## 2. Create `NEW/new.pub'. Determine key id. Cleanup: delete both.
405 ## 3. Rename to `NEW/TS.ID.priv'. Cleanup: continue to ANNOUNCE.
406 ## 4. Rename `MEW/new.pub' to `active/ID.pub'. (If it's not there,
407 ## then this has been done already.)
408 ## 5. Announce the key in the DNS.
409 ## 6. Promote the key to ANNOUNCE: rename `NEW/TS.ID.priv' to
410 ## `ANNOUNCE/TS.ID.priv'.
412 ## * `ANNOUNCE'. The key has been announced in the DNS, and we're waiting
413 ## for the announcement to propagate before we deploy the key. A
414 ## placeholder HTML file is written to `publish/I/D.html'. Unless we're
415 ## in catch-up mode, keys will remain in `ANNOUNCE' state for DNS-DELAY
416 ## seconds. The timestamp is the time at which we expect the key to
419 ## * `DEPLOY'. The key is ready for use in the upcoming cycle; the private
420 ## key file is in `active/ID.priv', and the key is named in the
421 ## `dkim-keys.state' file. The timestamp is the time at which we expect
422 ## the key to become active. The key will be active for ACTIVE-DURATION
425 ## * `RETIRE'. The key has been deployed and replaced, and will no longer
426 ## be used for signing. However, messages bearing its signatures are
427 ## potentially still in flight, so the key is still visible in DNS. Keys
428 ## will remain in `RETIRE' state for MAIL-PERSISTENCE seconds. The
429 ## timestamp is the time at which we expect that the key can be
432 ## * `WITHDRAW'. The key is no longer needed by verifiers because all
433 ## messages signed under it have either been delivered or abandoned. The
434 ## key is withdrawn from DNS, and we're waiting for the withdrawal to
435 ## propagate. Keys remain in `WITHDRAW' state for DNS-PERSISTENCE
436 ## seconds. The timestamp is the time at which we expect that the key
439 ## * `PUBLISH'. The key is no longer available to verifiers, because it is
440 ## no longer visible in DNS. It is therefore now safe to publish the
441 ## private key. This is the final state, and we stop tracking keys in
442 ## `PUBLISH' state. The `active/ID.priv' and `active/ID.pub' files are
443 ## deleted; the placeholder `publish/I/D.html' is replaced with a file
444 ## holding the actual key data. The private key file is deleted.
446 ## Mapping between state names and indices.
449 for my $st (qw{ NEW ANNOUNCE DEPLOY RETIRE WITHDRAW PUBLISH })
450 { $STIX{$st} = scalar @STNAME; push @STNAME, $st; }
452 ###--------------------------------------------------------------------------
453 ### Schedule objects.
456 ## A `Scheduler' object keeps track of things to do in the future. Most
457 ## notably, it accumulates DNS update tasks so that they can be performed
458 ## in a single transaction, and a list of miscellaneous subs to run.
460 ## Operations which create new files should be done immediately.
461 ## Operations which delete files (or rename them -- such as committing
462 ## state transitions by renaming the `.priv' file) should be deferred using
467 ## Create and return a new `Scheduler'.
469 return bless { dns_update => { }, cleanups => [] }, $pkg;
473 my ($me, $label, $type, $data) = @_;
474 ## Add a DNS record for LABEL, with the given TYPE and DATA.
476 ## This is collected as part of the single DNS update transaction. Any
477 ## existing record for the LABEL and TYPE is deleted, which is probably
478 ## terrible for general use, but works just fine in this program.
480 $me->{dns_update}{$label}{$type} = $data;
484 my ($me, $label, $type) = @_;
485 ## Delete the DNS record(s) for LABEL with the given TYPE.
487 $me->{dns_update}{$label}{$type} = undef;
492 ## Arrange to run OP at the end of the program.
494 push $me->{cleanups}->@*, $op;
499 ## Perform the scheduled activities.
501 ## Do the DNS update.
503 for my $label (keys $me->{dns_update}->%*) {
504 for my $type (keys $me->{dns_update}{$label}->%*) {
505 my $data = $me->{dns_update}{$label}{$type};
506 $updates .= "update delete $label IN $type\n";
507 if (!defined $data) {
508 print "delete dns record $label $type\n";
510 $updates .= "update add $label IN $type $data\n" if defined $data;
511 print "create dns record $label $type $data\n";
515 if ($updates eq "") {
516 print "no dns updates\n";
518 my @nsucmd = ("nsupdate");
519 push @nsucmd, "-k", $::C{"ddns-key"} if defined $::C{"ddns-key"};
521 @nsucmd = ("cat") if $ENV{"DKIM_KEYS_NODNS"} // 0;
522 open my $f, "|-", @nsucmd;
523 my $server = $::C{"ddns-server"}; my $ttl = $::C{"ddns-ttl"};
524 print $f "server $server\n" if defined $server;
525 print $f "ttl $ttl\n" if defined $ttl;
533 ## Run the deferred cleanup operations.
534 for my $op ($me->{cleanups}->@*) { $op->(); }
538 sub schedule_cleanup (&$) {
539 my ($op, $sched) = @_;
540 ## Get SCHED to run OP during its cleanup.
542 ## This just has slightly nicer syntax than calling `SCHED->add_cleanup'.
544 $sched->add_cleanup($op);
547 ###--------------------------------------------------------------------------
550 sub generate_key ($) {
552 ## Make a new RSA key and save it in PEM format in FILE.
554 ## No particular effort is taken to ensure that FILE is updated atomically.
556 my $oldmask = umask 0037;
557 print "generate new key\n";
558 run "openssl", "genrsa", "-out", $file, "3072";
562 sub extract_public_key ($$) {
563 my ($pub, $priv) = @_;
564 ## Store the public key corresponding to PRIV in the file PUB.
566 ## No particular effort is taken to ensure that PUB is updated atomically.
568 print "extract public key\n";
569 run "openssl", "rsa", "-pubout", "-in", $priv, "-out", $pub;
570 return read_pem($pub, "PUBLIC KEY");
574 ## A `Key' object keeps track of a DKIM key's lifecycle stages.
576 ## Key objects are blessed hashrefs with the following public slots.
578 ## * `id' is the key's identifier, as a lowercase Base32 string.
579 ## * `st' is the key's state, as an uppercase string.
580 ## * `t' is the key's timestamp, as an integer count of seconds.
582 ## It also has private slots.
584 ## * `file0' is the filename holding the private key.
585 ## * `file' is the filename that the private key /would/ have if the
586 ## currently scheduled operations were executed.
587 ## * `pub' is the key's public key, in binary BER format.
596 sub publish_components {
598 ## Split the key ID into pieces. This is how we map key IDs into the
599 ## filesystem and URI space: if we return n pieces, that's n - 1 levels
600 ## of directory, with leaves at the bottom.
603 return (substr($id, 0, 3), substr($id, 3, 5), substr($id, 8));
608 ## Return the URI at which the key will be published.
610 my @cc = $me->publish_components;
611 return $::C{"publish-uri"} . join("/", @cc) . ".html";
614 sub publish_filename {
616 ## Return the filename at which the key publication HTML file is stored.
618 my @cc = $me->publish_components;
619 my $name = "publish";
620 for my $c (@cc) { ::maybe_mkdir $name, 0751; $name .= "/" . $c; }
621 return $name . ".html";
626 ## Return a new `Key' object with initial timestamp T.
628 ## The `pub' slot is set on exit.
631 ::maybe_mkdir "NEW", 0700;
632 ::generate_key "NEW/new.priv";
634 ## Extract the public key and determine its id.
635 my $pub = ::extract_public_key "NEW/new.pub", "NEW/new.priv";
636 my $id = ::ident $pub;
638 ## Rename the private key. It won't go away now: we're committed to this
640 print "commit to new key $id\n";
641 my $file = sprintf "NEW/%s.%s.priv", ::present_stamp($t), $id;
642 rename "NEW/new.priv", $file;
644 ## Report the public key as active.
645 print "activated new public key $id\n";
646 ::maybe_mkdir "active";
647 rename "NEW/new.pub", "active/$id.pub";
649 ## It's now a proper key.
650 my $me = $pkg->load($file);
656 my ($pkg, $file) = @_;
657 ## Return a key object for the given FILE. We probably don't actually
658 ## bother reading the file.
660 ## Check that the file is sane.
661 -r $file or confess "key file `$file' not found";
663 (\d{4} - \d{2} - \d{2} T \d{2} : \d{2} : \d{2} Z) \.
664 ([abcdefghijklmnopqrstuvwxyz234567]+) \.priv $}x
665 or confess "bad key name `$file'";
667 ## Build the object.as the primary
668 my $st = $1; my $tm = $2; my $id = $3;
670 st => $st, id => $id, t => ::parse_stamp($tm),
671 file => $file, file0 => $file
676 my ($me, $sched, $newst, $newtime) = @_;
677 ## Change the state of the key to be NEWST, with timestamp NEWTIME.
678 ## Arrange to commit this state change by renaming the private key to
681 return if $newst eq $me->{st} && $newtime eq $me->{t};
683 printf "prepare key %s state %s %s -> %s %s\n",
685 ::present_time($me->{t}), $newst, ::present_time($newtime);
686 my $from = $me->{file};
687 my $to = $newst . "/" . ::present_stamp($newtime) . "." . $id . ".priv";
688 ::maybe_mkdir $newst;
690 print "key $id rename $from -> $to\n";
693 $me->{st} = $newst; $me->{t} = $newtime; $me->{file} = $to;
697 my ($me, $sched, $t_publish) = @_;
698 ## Announce the public key in DNS as `ID.ZONE', and create a placeholder
699 ## HTML file in `publish/I/D.html' explaining that the key will be
700 ## published before T_PUBLISH.
702 ## Initial preparation.
703 print "announce new key\n";
706 ## If we don't already have the public key then this must be a leftover
707 ## key from a previous aborted run. Create the public key file and
708 ## retrieve the key. Either way, we don't need it any more after this,
709 ## so save the memory.
710 my $pub = $me->{pub}; delete $me->{pub};
711 unless (defined $pub) {
712 ::maybe_mkdir "active";
713 $pub = ::extract_public_key "active/$id.pub", $me->{file0};
716 ## Prepare the record data.
718 "v=DKIM1; s=email; t=s:y; k=rsa; h=sha256; " .
719 "n=Not suitable for non-repudiation! " .
720 "Private key revealed at %s by %s; " .
723 ::present_time($t_publish),
724 encode_base64($pub, "");
726 ## A TXT record data consists of a sequence of strings of length at most
727 ## 255 each. The split positions are not significant. Split the data
728 ## into sufficiently small pieces, preferring to split at semantic
731 while (length $key > 255) {
732 my $chunk = substr $key, 0, 255, "";
733 if ($chunk =~ m{^ (.* \;\s+) ([^\s;] [^;]*) $}x)
734 { $chunk = $1; $key = $2 . $key; }
738 $key = join " ", map qq'"$_"', @key;
741 $sched->add_record($id . "." . $::C{"ddns-zone"}, "TXT", $key);
745 my ($me, $sched) = @_;
746 ## Make the key suitable for deployment. Hard link the private key to
750 print "deploy private key $id\n";
751 ::maybe_mkdir "active";
752 ::maybe_unlink "active/$id.priv.new";
753 link $me->{file0}, "active/$id.priv.new";
754 rename "active/$id.priv.new", "active/$id.priv";
758 my ($me, $sched) = @_;
759 ## Withdraw the key data from DNS so that legitimate reliers won't
760 ## believe signatures any more.
762 my $id = $me->{id}; my $zone = $::C{"ddns-zone"};
763 print "withdraw key $id from dns\n";
764 $sched->delete_record("$id.$zone", "TXT");
768 my ($me, $sched) = @_;
769 ## Delete the key from everywhere except the publication tree.
771 my $id = $me->{id}; print "delete key $id\n";
773 print "delete published key $id\n";
774 ::maybe_unlink "active/$id.pub";
775 ::maybe_unlink "active/$id.priv";
781 ###--------------------------------------------------------------------------
782 ### Publication archive.
784 sub publication_time ($;$$) {
785 my ($k, $newst, $newtime) = @_;
786 ## Return the approximate publication time of key K. If NEWST and NEWTIME
787 ## are given, then they override the key's state and timestamp.
789 $newst //= $k->{st}; $newtime //= $k->{t};
790 defined (my $newix = $STIX{$newst})
791 or confess "unexpected state `$newst'";
792 my $t_publish = $newtime + $C{"cycle-period"};
793 if ($newix < $STIX{PUBLISH})
794 { $t_publish += $C{"dns-persistence"}; }
795 if ($newix < $STIX{WITHDRAW})
796 { $t_publish += $C{"mail-persistence"}; }
797 if ($newix < $STIX{RETIRE})
798 { $t_publish += $C{"active-duration"} + $C{"cycle-period"}; }
802 sub publish_placeholder ($;$$) {
803 my ($k, $newst, $newtime) = @_;
804 ## Write a placeholder HTML file at the URI that key K's private key will
805 ## be published later. If NEWST and NEWTIME are given, then the override
806 ## the key's state and timestamp in the computation of the publication
809 ## Determine the things we need to know.
810 my $id = $k->{id}; my $inst = $C{"instance"};
811 my $file = $k->publish_filename;
812 my $t_publish = publication_time $k, $newst, $newtime;
813 my $th_publish = present_time $t_publish;
815 ## Produce the placeholder file.
816 print "publish placeholder for $id\n";
817 open my $f, ">", "$file.new";
822 <title>$inst DKIM key $id</title>
825 <h1>$inst DKIM key <code>$id</code></h1>
826 <p>This is a placeholder page.
827 The private key <code>$id</code> is scheduled for publication
828 on or before $th_publish.
829 $inst DKIM private keys are published in order to make them
830 unsuitable as evidence after the fact.
835 dump_file $f, "active/$id.pub";
842 rename "$file.new", $file;
845 sub publish_key ($) {
847 ## Publish key K's private key.
849 ## Determine the things we need to know.
850 my $id = $k->{id}; my $inst = $C{"instance"};
851 my $file = $k->publish_filename;
853 ## Produce the publication file.
854 print "publish private key for $id\n";
855 open my $f, ">", "$file.new";
860 <title>$inst DKIM key $id</title>
863 <h1>$inst DKIM key <code>$id</code></h1>
864 <p>The key <code>$id</code> was used as a key to authenticate
865 that email passed through the $inst server.
866 $inst DKIM keys are published after they are no longer in active use,
867 to make them unsuitable as evidence after the fact.
869 <p>The private and public keys are
872 dump_file $f, "active/$id.priv";
873 dump_file $f, "active/$id.pub";
880 rename "$file.new", $file;
883 ###--------------------------------------------------------------------------
884 ### State transitions.
886 sub set_key_state ($$$$) {
887 my ($k, $sched, $newst, $newtime) = @_;
888 ## Advance K to state NEWST, and set its timestamp to NEWTIME. Arrange for
889 ## SCHED to complete the transition.
891 ## Preliminary checking.
892 my $oldst = $k->{st}; my $id = $k->{id};
893 defined (my $ix = $STIX{$oldst})
894 or confess "unexpected key state `$oldst'";
895 defined (my $newix = $STIX{$newst})
896 or confess "unexpected new state `$newst'";
898 or confess "attempted state regression `$oldst' -> `$newst'";
900 ## Advance the key through each intermediate state in turn.
901 while ($ix < $newix) {
902 $ix++; my $curst = $STNAME[$ix];
903 print "advance key $id state $oldst -> $curst\n";
904 if ($curst eq "ANNOUNCE") {
905 $k->announce($sched, publication_time $k, $newst, $newtime);
906 publish_placeholder $k, $newst, $newtime;
908 elsif ($curst eq "DEPLOY") { $k->deploy($sched); }
909 elsif ($curst eq "RETIRE") { ; }
910 elsif ($curst eq "WITHDRAW") { $k->withdraw($sched); }
911 elsif ($curst eq "PUBLISH") { publish_key $k; }
912 else { confess "unexpected state $curst" }
915 ## Finally commit the key in its new state.
916 if ($newst eq "PUBLISH") { $k->delete($sched); }
917 else { $k->set_state($sched, $newst, $newtime); }
920 sub keys_in_state ($) {
922 ## Return a list of the keys in state ST.
925 eval { opendir $d, $st; };
927 die if $@->errno != ENOENT;
929 FILE: for my $f (readdir $d) {
930 next FILE if $f eq "." || $f eq "..";
931 next FILE if $st eq "NEW" && ($f eq "new.priv" || $f eq "new.pub");
932 push @k, Key->load("$st/$f");
938 ###--------------------------------------------------------------------------
941 ## Our first order of business is to ensure that there are keys lined up for
942 ## the current deployment cycle. The candidates are the keys which are
943 ## currently in `NEW', `ANNOUNCE', and `DEPLOY' states. We arrange to use
944 ## these in ascending order of their current timestamps. This might cause
945 ## keys to become active earlier than before.
947 my @candidates; # candidates for future deployment
948 my @to_retire; # keys which are too old
949 my $cutoff = $NOW - $C{"active-duration"}; # threshold between the two
950 my $sched = Scheduler->new; # a `Scheduler' instance
952 ## Work through the `NEW' keys and advance them on to `ANNOUNCE'. Pick up
953 ## all of the unretired keys so that we can plan how to use them.
955 sub notice_candidate ($) {
957 ## Notice a key and add it to one of the lists above. If K's deployment
958 ## time window is entirely in the past then add it to `@to_retire';
959 ## otherwise, add it to `@candidates'.
961 if ($k->{t} < $cutoff) { push @to_retire, $k; }
962 else { push @candidates, $k; }
965 for my $k (keys_in_state "NEW")
966 { $k->announce($sched, publication_time $k); notice_candidate $k; }
967 for my $st (qw{ ANNOUNCE DEPLOY })
968 { for my $k (keys_in_state $st) { notice_candidate $k; } }
969 @candidates = sort { $a->{t} <=> $b->{t} } @candidates;
971 ## Determine the current start time of the oldest key we can deploy. If this
972 ## is earlier than now then things are chugging along OK and we continue with
973 ## the current plan. Otherwise, we need to arrange keys for immediate use.
974 my $t0 = @candidates && $candidates[0]{t} <= $NOW ? $candidates[0]{t} : $NOW;
977 ## Start producing the state file for the mail server. Make sure that the
978 ## current cycle is covered.
980 my $t = $t0; my $t_limit = $NOW + $C{"cycle-period"};
981 while ($t < $t_limit) {
982 my $k = @candidates ? shift @candidates : Key->new($t);
983 set_key_state $k, $sched, "DEPLOY", $t; $t += $C{"active-duration"};
984 $info .= sprintf "info.%d: k = %s u = %s tpub = \"%s\"\n",
985 $nk, $k->{id}, $k->publish_uri, present_time publication_time $k;
989 ## Write the new state file.
990 maybe_mkdir "active";
991 open my $mcf, ">", "active/dkim-keys.state.new";
992 printf $mcf "### dkim-keys deployment state, from %s up to %s\n\n",
993 present_time($t0), present_time($t);
994 printf $mcf "params: t0 = %d step = %d n = %d\n",
995 $t0, $C{"active-duration"}, $nk;
997 close $mcf; rename "active/dkim-keys.state.new", "active/dkim-keys.state";
999 ## And now make sure there are enough new keys announced to cover the next
1001 $t_limit = $NOW + 2*$C{"cycle-period"};
1002 while ($t < $t_limit) {
1003 my $k = @candidates ? shift @candidates : Key->new($t);
1004 set_key_state $k, $sched, "ANNOUNCE", $t; $t += $C{"active-duration"};
1007 ## If there are keys to retire, then arrange that.
1008 for my $k (@to_retire) {
1009 set_key_state $k, $sched, "RETIRE",
1010 $k->{t} + $C{"active-duration"} + $C{"mail-persistence"};
1013 ## If there are keys to withdraw, then arrange that too.
1014 for my $k (keys_in_state "RETIRE") {
1015 if ($k->{t} <= $NOW)
1016 { set_key_state $k, $sched, "WITHDRAW", $NOW + $C{"dns-persistence"}; }
1019 ## And, finally, if there are keys to be published, then arrange that.
1020 for my $k (keys_in_state "WITHDRAW")
1021 { if ($k->{t} <= $NOW) { set_key_state $k, $sched, "PUBLISH", -1; } }
1023 ## The planning is done. It's now time to do all the things.
1024 print "running cleanup actions\n";
1027 ###--------------------------------------------------------------------------
1032 dkim-keys - manage short-lived DKIM keys
1042 DKIM , RFC6376, is a mechanism for authenticating email messages. An
1043 originating mail server signs each message that it sends using a private
1044 signing key, and adds a header to the message containing the signature and a
1045 reference to where the public verification key can be found in the DNS.
1046 A receiving mail server can parse the header, retrieve the verification key,
1047 verify the signature, and be convinced that the message at least passed
1048 through the originating server. This is intended to reduce the effectiveness
1049 of forged email messages for fraud and spam.
1051 A DKIM signature must cover email headers I<and> the body, to prevent an
1052 adversary from altering them in order to construct a forgery. This creates
1053 a problem which the designers of DKIM failed to foresee. Messages bearing
1054 DKIM signatures can remain verifiable long after delivery, providing
1055 convincing evidence that a particular mail server transmitted a particular
1056 message, and, moreover, at a particular time, and that was sent by a
1057 particular user. This may be undesirable, to say the least.
1059 In 2020, Matthew Green wrote an article describing this problem on his
1060 I<Cryptography Engineering> blog, and suggesting a solution: that DKIM
1061 signing keys should cycle rapidly, each key being used only for a short time,
1062 say a day, and, once all messages bearing signatures from a particular key
1063 have been delivered or abandoned, the I<private> signing key should be
1064 published. Once this is done, the DKIM signature on an old message becomes
1065 worthless as evidence of anything, since anyone with a little technical
1066 ability can forge convincing-looking messages bearing a valid signature from
1069 This is what B<dkim-keys> does.
1071 =head2 Inovking B<dkim-keys>
1073 The B<dkim-keys> doesn't usually take any command-line arguments. It does
1074 recognize B<--help> (B<-h>) and B<--version> (B<-v>) options, for
1075 consistency's sake. It reports an error if other command-line arguments are
1080 The B<dkim-keys> program works in the current directory. It expects to find
1081 a configuration file B<dkim-keys.conf>. It will create directories for its
1082 own use as necessary.
1086 Each key has an I<identifier>, notated I<id> here. For RSA keys, which are
1087 the only kind currently implemented, this is the least significant 80 bits of
1088 the modulus, encoded in lowercase Base32. (This means that the identifier
1089 can easily be determined from just the public key.) There are no padding
1090 characters because 80 is a multiple of five. The key identifier is used in
1091 the DKIM selector when the key is in use.
1093 Keys advance through six states.
1099 A key in the B<NEW> state has just been created, and nobody else knows
1100 anything about it. Under normal circumstances, keys immediately advance to
1101 B<ANNOUNCE>, but the B<NEW> state is separate for technical reasons.
1105 A key in the B<ANNOUNCE> state has a record listed in the DNS, and we're
1106 waiting for the DNS records to propagate before the key can be used.
1110 A key in the B<DEPLOY> state is ready for use by a mail server to sign
1111 outgoing messages. There are usually multiple keys in this state so that the
1112 mail server can cycle from one to the next at the appropriate time without
1113 any further action from B<dkim-keys>.
1117 A key in the B<RETIRE> state has completed its time in service. It may have
1118 been used to sign outgoing messages which are still on their way to being
1119 delivered or abandoned, so legitimate verifiers need access to the public
1120 key, but it won't be used to sign new messages.
1124 A key in the B<WITHDRAW> state should no longer be of use to receiving
1125 servers. All messages signed using the key have either been delivered or
1126 abandoned. The public key record is withdrawn from the DNS, and we're
1127 waiting for this withdrawal to propagate before the private key can be
1128 published. At this stage, a forgery made using the key might be accepted by
1129 a receiving server which still has access to stale DNS records, so the
1130 private key can't quite be published yet.
1134 A key in the B<PUBLISH> state is published for all to see. Nobody should
1135 beleive it for anything, and nobody should have any reason to.
1139 =head2 Configuration file
1141 The configuration syntax is simple and line-based. Lines consisting only of
1142 whitespace, and lines whose first whitespace character is C<#>, are ignored.
1143 Other lines must be I<assignments> of the form I<key> B<=> I<value>. The
1144 recognized keys and their values are as follows.
1150 A name for the instance of B<dkim-keys>. This is used in the published HTML
1151 files. There is no default.
1153 =item B<publish-uri>
1155 The base URL at which keys will be published. The web server should be
1156 configured to publish the contents of the B<publish> directory at this URL.
1157 There is no default.
1161 The DNS I<zone> in which the public keys will be listed. A key with a given
1162 I<id> will be listed in a B<TXT> record at I<id>B<.>I<zone>. In a simple
1163 system, I<zone> might be B<_domainkey.example.org>, so the B<example.org>
1164 outgoing mail server need only set B<s=>I<id>B<;> B<d=example.org> in its
1165 DKIM headers. In a more complex case, the I<zone> might be referred to using
1166 B<DNAME> for multiple domains.
1168 There is no default for this setting.
1170 =item B<ddns-server>
1172 The server to which DNS updates will be submitted. By default, the source
1173 server listed in the zone B<SOA> record will be used.
1177 The file containing the TKIP key to use for authenticating the DNS update.
1178 There is no default for this setting.
1182 The TTL to set on DKIM key records, as a duration (see below). The default
1187 The time required for a newly published DNS record to fully propagate, as a
1188 duration (see below). Conservatively, this will be the sum of the zone
1189 refresh, expiry, and minimum-TTL times: a secondary server must have time to
1190 try updating, give up, and expire the zone, and a downstream recursive
1191 resolver must time out a previously cached negative result for the record,
1192 before the record can be considered fully propagated.
1194 This setting determines how long a key remains in the B<ANNOUNCE> state. The
1195 default is two days.
1197 =item B<active-duration>
1199 The time for which a key will be in active use signing outgoing messages, as
1200 a duration (see below). The default is one day.
1202 =item B<cycle-period>
1204 The maximum interval between runs of B<dkim-keys>, as a duration (see below).
1205 This determines the how many keys are queued up in the B<DEPLOY> state. The
1206 default is three days, with the expectation that the script will actually run
1209 =item B<mail-persistence>
1211 The time before which outgoing email messages can safely be expected to have
1212 been delivered or abandoned, as a duration (see below). This mostly depends
1213 on the outgoing server configuration, but there can conceivably be additional
1214 delay at the receiving end.
1216 This setting determines how long a key remains in the B<RETIRE> state. The
1217 default is one week.
1219 =item B<dns-persistence>
1221 The time required for a withdrawn DNS record to fully propagate, as a
1222 duration (see below). Conservatively, this will be the sum of the zone
1223 refresh and expiry times, and the record TTL: a secondary server must have
1224 time to try updating, give up, and expire the zone, and a downstream
1225 recursive resolver must time out a previously cached record, before it can be
1226 considered fully withdrawn.
1228 This setting determines how long a key remains in the B<WITHDRAW> state. The
1229 default is three days.
1233 A duration is a (possibly fractional) decimal number, followed by an optional
1240 B<s>, B<sec>, B<secs>, B<second>, B<seconds>
1244 B<min>, B<m>, B<mins>, B<minute>, B<minutes>
1248 B<hr>, B<h>, B<hrs>, B<hour>, B<hours>
1252 B<dy>, B<d>, B<dys>, B<day>, B<days>
1256 B<wk>, B<w>, B<wks>, B<week>, B<weeks>
1260 If no unit is given, the default is seconds.
1264 The B<active> directory contains public and private key files, and a
1265 B<dkim-keys.state> file for the mail server.
1267 B<active/>I<id>B<.pub> contains the public key for the key with identifier
1268 I<id> (in OpenSSL PEM format), and B<active/>I<id>B<.priv> is the private key
1269 (a hard link to the I<state>B</>I<timestamp>B<.>I<id>B<.priv> file).
1271 The B<active/dkim-keys.state> file is formatted as follows. It contains
1272 blank lines and (sparse) comments beginning with C<#>. It contains a line
1276 B<params:> B<t0 => I<t0> B<step => I<step> B<n => I<n>
1280 where I<t0>, I<step>, and I<n> are integers in decimal notation. It also
1285 B<info.>I<i>B<:> B<k => I<id> B<u => I<url> B<tpub => I<tpub>
1289 where I<i> is an integer, I<id> is a key identifier, I<url> is a url (not
1290 containing spaces), and I<tpub> is an ISO8601 time stamp with explicit time
1291 zone offset, in double quotes. There will be exactly I<n> B<info> lines, one
1292 for each I<i> from 0 (inclusive) up to but not including I<n>. These lines
1293 may appear in any order.
1295 The B<params> line identifies one of the B<info> lines, as follows. I<t0> is
1296 a POSIX timestamp, counting nonleap seconds since the start of January 1970.
1297 I<step> is the length of time, in seconds, for which a key is active (taken
1298 from the B<active-duration> configuration setting). If the current POSIX
1299 time is I<t>, then the mail server should use the B<info.>I<i> data, where
1300 I<i> = floor((I<t> - I<t0>)/I<step>). If I<i>, as computed in this way, is
1301 less than zero or greater than or equal to I<n>, then the data is invalid:
1302 the mail server should report temporary failure. Specifically, if I<i> is
1303 less than zero then the data is apparently from the future: this is most
1304 likely if the system clock has been stepped, and the situation can be
1305 corrected by running B<dkim-keys> again. If I<i> is greater than or equal to
1306 I<n> then the file is out-of-date: B<dkim-keys> has not run to completion
1307 sufficiently recently, or is otherwise broken.
1309 The B<info> line fields are as follows. The I<id> is the identifier of the
1310 signing key to use, and to include in the DKIM B<s=> selector tag; the actual
1311 private key will be B<active/>I<id>B<.priv>. The I<url> is the URL at which
1312 the private key will be published (derived from the key identifier and the
1313 B<publish-uri> configuration setting) and I<tpub> is a time by which
1314 publication should have occurred; the latter two items are intended to be
1315 included in a message header.
1317 Exim, for example, might be configured as follows.
1320 ${lookup {params} lsearch \
1321 {${lookup {${domain:$h_From:}} partial0-lsearch \
1322 {/etc/mail/dkim-sign.conf} \
1323 {/var/lib/dkim-keys/$value/active/dkim-keys.state}}} \
1324 {${if and {{>= {$tod_epoch} {${extract {t0}{$value}}}} \
1326 {${eval:${extract {t0}{$value}} + \
1327 ${extract {n}{$value}}*${extract {step}{$value}}}}}} \
1328 {${lookup {info.${eval:($tod_epoch - ${extract {t0}{$value}})/ \
1329 ${extract {step}{$value}}}}
1331 {${lookup {${domain:$h_From:}} partial0-lsearch \
1332 {/etc/mail/dkim-sign.conf} \
1333 {/var/lib/dkim-keys/$value/active/dkim-keys.state}}} \
1334 {${extract {k}{$value}}}fail}} \
1337 dkim_private_key = \
1338 ${lookup {${domain:$h_From:}} partial0-lsearch \
1339 {/etc/mail/dkim-sign.conf} \
1340 /var/lib/dkim-keys/$value/active/$dkim_selector.priv}
1342 =head2 Working state
1344 In the working directory, there is a subdirectory for each state. Private
1345 keys are stored (in OpenSSL PEM) format) in files named
1346 I<state>B</>I<timestamp>B<.>I<id>B<.priv> where I<state> is the key's state,
1347 as listed above, in uppercase, I<timestamp> is a timestamp in ISO8601 `zulu'
1348 format, and I<id> is the key's identifier, used as the DKIM selector. The
1349 meaning and value of the timestamp changes as the key advances from one state
1350 to the next: see the source code for details.
1358 D. Crocker, T. Hansen, M. Kucherawy, RFC6376: I<DomainKeys Identified Mail
1359 (DKIM) Signatures>, L<https://www.rfc-editor.org/rfc/rfc6376.html>.
1363 Matthew Green, I<Ok Google: Please publish your DKIM secret keys>,
1364 L<https://blog.cryptographyengineering.com/2020/11/16/ok-google-please-publish-your-dkim-secret-keys/>
1374 Mark Wooding, <mdw@distorted.org.uk>.
1378 ###----- That's all, folks --------------------------------------------------