chiark / gitweb /
Makefile: Support out-of-tree builds (badly).
[distorted-dkim] / dkim-keys.in
1 #! /usr/bin/perl -w
2
3 ###----- Licensing notice ---------------------------------------------------
4 ###
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.
9 ###
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.
14 ###
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.
18
19 ###--------------------------------------------------------------------------
20 ### External modules.
21
22 use autodie;
23 use strict;
24
25 use Carp;
26 use IO::Handle;
27 use MIME::Base32;
28 use MIME::Base64;
29 use POSIX;
30 use Time::Local;
31
32 ###--------------------------------------------------------------------------
33 ### Other preliminaries.
34
35 (our $PROG = $0) =~ s!^.*/!!;
36 our $VERSION = '@VERSION@';
37
38 ## Check arguments.  This is easy: there aren't any.
39 sub usage (*) { my ($f) = @_; print $f "usage: $PROG\n"; }
40 if (@ARGV) {
41   my $arg = $ARGV[0];
42   if (@ARGV > 1) { }
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; }
47   usage STDERR; exit 2;
48 }
49
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.
54
55   eval { mkdir $dir, $mode // 0777; }; die if $@ && $@->errno != EEXIST;
56 }
57
58 sub maybe_unlink ($) {
59   my ($file) = @_;
60   ## Delete FILE.  Ignore errors complaining that the FILE doesn't exist.
61
62   eval { unlink $file; }; die if $@ and $@->errno != ENOENT;
63 }
64
65 sub present_time ($) {
66   my ($t) = @_;
67   ## Format T as a human-readable time.
68
69   return strftime "%Y-%m-%d %H:%M:%S %z", localtime $t;
70 }
71
72 sub dump_file ($$) {
73   my ($fout, $file) = @_;
74   ## Copy the contents of FILE to the stream FOUT.
75
76   open my $fin, "<", $file;
77   my $buf;
78   while (sysread $fin, $buf, 65536) { print $fout $buf; }
79   close $fin;
80 }
81
82 sub parse_stamp ($) {
83   my ($stamp) = @_;
84   ## Parse an ISO8601 zulu-time stamp to POSIX `time_t'.
85
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;
91 }
92
93 sub present_stamp ($) {
94   my ($t) = @_;
95   ## Format a POSIX `time_t' as a string.
96
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;
100 }
101
102 sub running (&@) {
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.
106
107   my $label = sprintf "RUN %s ", join " ", @prog;
108   print $label . ">" x (77 - length $label) . "\n";
109   my @r;
110   if (wantarray) { @r = $body->(); }
111   else { @r = scalar $body->(); }
112   print "<" x 77 . "\n";
113   if (!$?) { }
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 $?;
117   return @r;
118 }
119
120 sub run (@) {
121   my (@prog) = @_;
122   ## Run PROG.
123
124   running { system @prog; } @prog;
125 }
126
127 ## Set the current time.
128 our $NOW;
129 if (!defined $ENV{"DKIM_KEYS_TIMENOW"}) {
130   $NOW = time;
131 } else {
132   $NOW = parse_stamp $ENV{"DKIM_KEYS_TIMENOW"};
133   printf "pretending time is %s\n", present_time $NOW;
134 }
135
136 printf "%s BEGINS at %s\n", $PROG, present_time $NOW;
137
138 ###--------------------------------------------------------------------------
139 ### BER decoding.
140
141 ## BER tag classes.
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
146
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
155
156 sub ber_decoid ($) {
157   my ($oid) = @_;
158   ## Decode an encoded OID body.  Return the OID in text format, as a list
159   ## of integers separated by `.'.
160
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;
164 }
165
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
172   ## encoded body.
173
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; }
177
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)"; }
181   elsif ($n > 0x80) {
182     $n &= 0x7f;
183     my @n = unpack +(sprintf "C%d", $n), $r;
184     $r = substr $r, $n;
185     $n = 0; for my $i (@n) { $n = 256*$n + $i; }
186   }
187   $$str_inout = substr $r, $n; return ($c, $k, $t, substr $r, 0, $n);
188 }
189
190 sub read_pem ($$) {
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.
194
195   my $body = "";
196   open my $f, "<", $file;
197   LINE: while (<$f>)
198    { last LINE if /^-----BEGIN \Q$label\E-----$/; }
199   LINE: while (<$f>)
200     { last LINE if /^-----END \Q$label\E-----$/; $body .= $_; }
201   close $f;
202
203   return decode_base64 $body;
204 }
205
206 sub ident ($) {
207   my ($pub) = @_;
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.
211
212   my ($c, $k, $t);
213
214   ## Outer SEQUENCE.
215   ($c, $k, $t, my $x) = ber_decnext $pub;
216   $c == C_UNV && $k && $t == TY_SEQ
217     or die "malformed key (expected seq)";
218
219   ## Inner SEQUENCE.
220   ($c, $k, $t, my $y) = ber_decnext $x;
221   $c == C_UNV && $k && $t == TY_SEQ
222     or die "malformed key (expected seq)";
223
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)";
230
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)";
235
236   ## End inner SEQUENCE.
237   $y eq "" or die "malformed key (trailing junk)";
238
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)";
246
247   ## Inner SEQUENCE.
248   ($c, $k, $t, $y) = ber_decnext $u;
249   $c == C_UNV && $k && $t == TY_SEQ
250     or die "malformed key (expected seq)";
251
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)";
256
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)";
261
262   ## Close the inner SEQUENCE, BIT STRING, outer SEQUENCE, and the
263   ## top-level.
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)";
268
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), "";
272 }
273
274 ###--------------------------------------------------------------------------
275 ### Configuration.
276
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.
281
282 sub conf_token ($$) {
283   my ($parsep, $val) = @_;
284   ## Do-nothing conversion function for text.
285
286   return $val;
287 }
288
289 sub conf_stamp ($$) {
290   my ($parsep, $val) = @_;
291   ## Convert ISO8601 zulu-time stamps to POSIX `time_t'.
292
293   if ($parsep) { return parse_stamp $val; }
294   else { return present_stamp $val; }
295 }
296
297 {
298   ## Map between unit names and scales.
299   my %unitmap; my @unitmap;
300
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]];
309   }
310
311   sub conf_duration ($$) {
312     my ($parsep, $val) = @_;
313     ## Convert time durations with units.
314
315     if ($parsep) {
316       my ($x, $u) = $val =~ m{^ \s* (\.\d+ | \d+ (?: \. \d*)?)
317                                 \s* (| \S+) $}x
318         or die "bad duration `$val'";
319       my $scale = $unitmap{$u} or die "bad duration `$val'";
320       return $x*$scale;
321     } else {
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; }
326       }
327       return sprintf "%.4g %s", $x, $u;
328     }
329   }
330 }
331
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.
336   ##
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
339   ## default value.
340
341   my %c;
342   open my $f, "<", $file;
343   LINE: while (<$f>) {
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);
351   }
352   close $f;
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'"; }
357   }
358   return \%c;
359 }
360
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.
365
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});
370   }
371   close $f;
372   rename "$file.new", $file;
373 }
374
375 ###--------------------------------------------------------------------------
376 ### Configuration.
377
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]);
389
390 our %C = %{read_config \%CONF_SPEC, "dkim-keys.conf"};
391
392 ###--------------------------------------------------------------------------
393 ### State model.
394
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.
399 ##
400 ##   * `NEW'.  This is rather complicated, and there are multiple steps and
401 ##     a fiddly cleanup procedure.
402 ##
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'.
411 ##
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
417 ##     become active.
418 ##
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
423 ##     seconds.
424 ##
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
430 ##     withdrawn.
431 ##
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
437 ##     can be published.
438 ##
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.
445
446 ## Mapping between state names and indices.
447 our @STNAME;
448 our %STIX = ();
449 for my $st (qw{ NEW ANNOUNCE DEPLOY RETIRE WITHDRAW PUBLISH })
450   { $STIX{$st} = scalar @STNAME; push @STNAME, $st; }
451
452 ###--------------------------------------------------------------------------
453 ### Schedule objects.
454
455 { package Scheduler;
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.
459   ##
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
463   ## `add_cleanup'.
464
465   sub new {
466     my ($pkg) = @_;
467     ## Create and return a new `Scheduler'.
468
469     return bless { dns_update => { }, cleanups => [] }, $pkg;
470   }
471
472   sub add_record {
473     my ($me, $label, $type, $data) = @_;
474     ## Add a DNS record for LABEL, with the given TYPE and DATA.
475     ##
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.
479
480     $me->{dns_update}{$label}{$type} = $data;
481   }
482
483   sub delete_record {
484     my ($me, $label, $type) = @_;
485     ## Delete the DNS record(s) for LABEL with the given TYPE.
486
487     $me->{dns_update}{$label}{$type} = undef;
488   }
489
490   sub add_cleanup {
491     my ($me, $op) = @_;
492     ## Arrange to run OP at the end of the program.
493
494     push $me->{cleanups}->@*, $op;
495   }
496
497   sub execute {
498     my ($me) = @_;
499     ## Perform the scheduled activities.
500
501     ## Do the DNS update.
502     my $updates = "";
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";
509         } else {
510           $updates .= "update add $label IN $type $data\n" if defined $data;
511           print "create dns record $label $type $data\n";
512         }
513       }
514     }
515     if ($updates eq "") {
516       print "no dns updates\n";
517     } else {
518       my @nsucmd = ("nsupdate");
519       push @nsucmd, "-k", $::C{"ddns-key"} if defined $::C{"ddns-key"};
520       ::running {
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;
526         print $f $updates;
527         print $f "send\n";
528         print $f "answer\n";
529         close $f
530       } @nsucmd;
531     }
532
533     ## Run the deferred cleanup operations.
534     for my $op ($me->{cleanups}->@*) { $op->(); }
535   }
536 }
537
538 sub schedule_cleanup (&$) {
539   my ($op, $sched) = @_;
540   ## Get SCHED to run OP during its cleanup.
541   ##
542   ## This just has slightly nicer syntax than calling `SCHED->add_cleanup'.
543
544   $sched->add_cleanup($op);
545 }
546
547 ###--------------------------------------------------------------------------
548 ### Key management.
549
550 sub generate_key ($) {
551   my ($file) = @_;
552   ## Make a new RSA key and save it in PEM format in FILE.
553   ##
554   ## No particular effort is taken to ensure that FILE is updated atomically.
555
556   my $oldmask = umask 0037;
557   print "generate new key\n";
558   run "openssl", "genrsa", "-out", $file, "3072";
559   umask $oldmask;
560 }
561
562 sub extract_public_key ($$) {
563   my ($pub, $priv) = @_;
564   ## Store the public key corresponding to PRIV in the file PUB.
565   ##
566   ## No particular effort is taken to ensure that PUB is updated atomically.
567
568   print "extract public key\n";
569   run "openssl", "rsa", "-pubout", "-in", $priv, "-out", $pub;
570   return read_pem($pub, "PUBLIC KEY");
571 }
572
573 { package Key;
574   ## A `Key' object keeps track of a DKIM key's lifecycle stages.
575   ##
576   ## Key objects are blessed hashrefs with the following public slots.
577   ##
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.
581   ##
582   ## It also has private slots.
583   ##
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.
588
589   ## External modules.
590   use autodie;
591   use strict;
592
593   use Carp;
594   use MIME::Base64;
595
596   sub publish_components {
597     my ($me) = @_;
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.
601
602     my $id = $me->{id};
603     return (substr($id, 0, 3), substr($id, 3, 5), substr($id, 8));
604   }
605
606   sub publish_uri {
607     my ($me) = @_;
608     ## Return the URI at which the key will be published.
609
610     my @cc = $me->publish_components;
611     return $::C{"publish-uri"} . join("/", @cc) . ".html";
612   }
613
614   sub publish_filename {
615     my ($me) = @_;
616     ## Return the filename at which the key publication HTML file is stored.
617
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";
622   }
623
624   sub new {
625     my ($pkg, $t) = @_;
626     ## Return a new `Key' object with initial timestamp T.
627     ##
628     ## The `pub' slot is set on exit.
629
630     ## Make the new key.
631     ::maybe_mkdir "NEW", 0700;
632     ::generate_key "NEW/new.priv";
633
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;
637
638     ## Rename the private key.  It won't go away now: we're committed to this
639     ## one.
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;
643
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";
648
649     ## It's now a proper key.
650     my $me = $pkg->load($file);
651     $me->{pub} = $pub;
652     return $me;
653   }
654
655   sub load {
656     my ($pkg, $file) = @_;
657     ## Return a key object for the given FILE.  We probably don't actually
658     ## bother reading the file.
659
660     ## Check that the file is sane.
661     -r $file or confess "key file `$file' not found";
662     $file =~ m{^ (\w+) /
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'";
666
667     ## Build the object.as the primary
668     my $st = $1; my $tm = $2; my $id = $3;
669     return bless {
670       st => $st, id => $id, t => ::parse_stamp($tm),
671       file => $file, file0 => $file
672     }, $pkg;
673   }
674
675   sub set_state {
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
679     ## `ST/TS.ID.priv'.
680
681     return if $newst eq $me->{st} && $newtime eq $me->{t};
682     my $id = $me->{id};
683     printf "prepare key %s state %s %s -> %s %s\n",
684       $id, $me->{st},
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;
689     ::schedule_cleanup {
690       print "key $id rename $from -> $to\n";
691       rename $from, $to;
692     } $sched;
693     $me->{st} = $newst; $me->{t} = $newtime; $me->{file} = $to;
694   }
695
696   sub announce {
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.
701
702     ## Initial preparation.
703     print "announce new key\n";
704     my $id = $me->{id};
705
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};
714     }
715
716     ## Prepare the record data.
717     my $key = sprintf
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; " .
721       "p=%s",
722       $me->publish_uri,
723       ::present_time($t_publish),
724       encode_base64($pub, "");
725
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
729     ## boundaries.
730     my @key = ();
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; }
735       push @key, $chunk;
736     }
737     push @key, $key;
738     $key = join " ", map qq'"$_"', @key;
739
740     ## Add the record.
741     $sched->add_record($id . "." . $::C{"ddns-zone"}, "TXT", $key);
742   }
743
744   sub deploy {
745     my ($me, $sched) = @_;
746     ## Make the key suitable for deployment.  Hard link the private key to
747     ## `active/ID.priv'.
748
749     my $id = $me->{id};
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";
755   }
756
757   sub withdraw {
758     my ($me, $sched) = @_;
759     ## Withdraw the key data from DNS so that legitimate reliers won't
760     ## believe signatures any more.
761
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");
765   }
766
767   sub delete {
768     my ($me, $sched) = @_;
769     ## Delete the key from everywhere except the publication tree.
770
771     my $id = $me->{id}; print "delete key $id\n";
772     ::schedule_cleanup {
773       print "delete published key $id\n";
774       ::maybe_unlink "active/$id.pub";
775       ::maybe_unlink "active/$id.priv";
776       unlink $me->{file};
777     } $sched;
778   }
779 }
780
781 ###--------------------------------------------------------------------------
782 ### Publication archive.
783
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.
788
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"}; }
799   return $t_publish;
800 }
801
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
807   ## time.
808
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;
814
815   ## Produce the placeholder file.
816   print "publish placeholder for $id\n";
817   open my $f, ">", "$file.new";
818   print $f <<EOF;
819 <!DOCTYPE html>
820 <html>
821 <head>
822   <title>$inst DKIM key $id</title>
823 </head>
824 <body>
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.
831
832 <p>The public key is
833 <pre>
834 EOF
835   dump_file $f, "active/$id.pub";
836   print $f <<EOF;
837 </pre>
838 </body>
839 </html>
840 EOF
841   close $f;
842   rename "$file.new", $file;
843 }
844
845 sub publish_key ($) {
846   my ($k) = @_;
847   ## Publish key K's private key.
848
849   ## Determine the things we need to know.
850   my $id = $k->{id}; my $inst = $C{"instance"};
851   my $file = $k->publish_filename;
852
853   ## Produce the publication file.
854   print "publish private key for $id\n";
855   open my $f, ">", "$file.new";
856   print $f <<EOF;
857 <!DOCTYPE html>
858 <html>
859 <head>
860   <title>$inst DKIM key $id</title>
861 </head>
862 <body>
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.
868
869 <p>The private and public keys are
870 <pre>
871 EOF
872   dump_file $f, "active/$id.priv";
873   dump_file $f, "active/$id.pub";
874   print $f <<EOF;
875 </pre>
876 </body>
877 </html>
878 EOF
879   close $f;
880   rename "$file.new", $file;
881 }
882
883 ###--------------------------------------------------------------------------
884 ### State transitions.
885
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.
890
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'";
897   $ix <= $newix
898     or confess "attempted state regression `$oldst' -> `$newst'";
899
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;
907     }
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" }
913   }
914
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); }
918 }
919
920 sub keys_in_state ($) {
921   my ($st) = @_;
922   ## Return a list of the keys in state ST.
923
924   my @k; my $d;
925   eval { opendir $d, $st; };
926   if ($@) {
927     die if $@->errno != ENOENT;
928   } else {
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");
933     }
934   }
935   return @k;
936 }
937
938 ###--------------------------------------------------------------------------
939 ### Main program.
940
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.
946
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
951
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.
954
955 sub notice_candidate ($) {
956   my ($k) = @_;
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'.
960
961   if ($k->{t} < $cutoff) { push @to_retire, $k; }
962   else { push @candidates, $k; }
963 }
964
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;
970
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;
975 my $nk = 0;
976
977 ## Start producing the state file for the mail server.  Make sure that the
978 ## current cycle is covered.
979 my $info = "";
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;
986   $nk++;
987 }
988
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;
996 print $mcf $info;
997 close $mcf; rename "active/dkim-keys.state.new", "active/dkim-keys.state";
998
999 ## And now make sure there are enough new keys announced to cover the next
1000 ## cycle.
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"};
1005 }
1006
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"};
1011 }
1012
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"}; }
1017 }
1018
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; } }
1022
1023 ## The planning is done.  It's now time to do all the things.
1024 print "running cleanup actions\n";
1025 $sched->execute;
1026
1027 ###--------------------------------------------------------------------------
1028 ### Manual.
1029
1030 =head1 NAME
1031
1032 dkim-keys - manage short-lived DKIM keys
1033
1034 =head1 SYNOPSIS
1035
1036 B<dkim-keys>
1037
1038 =head1 DESCRIPTION
1039
1040 =head2 Background
1041
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.
1050
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.
1058
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
1067 the key.
1068
1069 This is what B<dkim-keys> does.
1070
1071 =head2 Inovking B<dkim-keys>
1072
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
1076 given.
1077
1078 =head2 Operation
1079
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.
1083
1084 =head2 State model
1085
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.
1092
1093 Keys advance through six states.
1094
1095 =over
1096
1097 =item *
1098
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.
1102
1103 =item *
1104
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.
1107
1108 =item *
1109
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>.
1114
1115 =item *
1116
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.
1121
1122 =item *
1123
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.
1131
1132 =item *
1133
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.
1136
1137 =back
1138
1139 =head2 Configuration file
1140
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.
1145
1146 =over
1147
1148 =item B<instance>
1149
1150 A name for the instance of B<dkim-keys>.  This is used in the published HTML
1151 files.  There is no default.
1152
1153 =item B<publish-uri>
1154
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.
1158
1159 =item B<ddns-zone>
1160
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.
1167
1168 There is no default for this setting.
1169
1170 =item B<ddns-server>
1171
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.
1174
1175 =item B<ddns-key>
1176
1177 The file containing the TKIP key to use for authenticating the DNS update.
1178 There is no default for this setting.
1179
1180 =item B<ddns-ttl>
1181
1182 The TTL to set on DKIM key records, as a duration (see below).  The default
1183 is four hours.
1184
1185 =item B<dns-delay>
1186
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.
1193
1194 This setting determines how long a key remains in the B<ANNOUNCE> state.  The
1195 default is two days.
1196
1197 =item B<active-duration>
1198
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.
1201
1202 =item B<cycle-period>
1203
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
1207 daily.
1208
1209 =item B<mail-persistence>
1210
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.
1215
1216 This setting determines how long a key remains in the B<RETIRE> state.  The
1217 default is one week.
1218
1219 =item B<dns-persistence>
1220
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.
1227
1228 This setting determines how long a key remains in the B<WITHDRAW> state.  The
1229 default is three days.
1230
1231 =back
1232
1233 A duration is a (possibly fractional) decimal number, followed by an optional
1234 unit.
1235
1236 =over
1237
1238 =item *
1239
1240 B<s>, B<sec>, B<secs>, B<second>, B<seconds>
1241
1242 =item *
1243
1244 B<min>, B<m>, B<mins>, B<minute>, B<minutes>
1245
1246 =item *
1247
1248 B<hr>, B<h>, B<hrs>, B<hour>, B<hours>
1249
1250 =item *
1251
1252 B<dy>, B<d>, B<dys>, B<day>, B<days>
1253
1254 =item *
1255
1256 B<wk>, B<w>, B<wks>, B<week>, B<weeks>
1257
1258 =back
1259
1260 If no unit is given, the default is seconds.
1261
1262 =head2 Output
1263
1264 The B<active> directory contains public and private key files, and a
1265 B<dkim-keys.state> file for the mail server.
1266
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).
1270
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
1273
1274 =over
1275
1276 B<params:> B<t0 => I<t0> B<step => I<step> B<n => I<n>
1277
1278 =back
1279
1280 where I<t0>, I<step>, and I<n> are integers in decimal notation.  It also
1281 contains lines
1282
1283 =over
1284
1285 B<info.>I<i>B<:> B<k => I<id> B<u => I<url> B<tpub => I<tpub>
1286
1287 =back
1288
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.
1294
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.
1308
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.
1316
1317 Exim, for example, might be configured as follows.
1318
1319         dkim_selector = \
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}}}} \
1325                        {< {$tod_epoch} \
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}}}}
1330                       lsearch \
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}} \
1335               fail}}\
1336             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}
1341
1342 =head2 Working state
1343
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.
1351
1352 =head1 SEE ALSO
1353
1354 =over
1355
1356 =item *
1357
1358 D. Crocker, T. Hansen, M. Kucherawy, RFC6376: I<DomainKeys Identified Mail
1359 (DKIM) Signatures>, L<https://www.rfc-editor.org/rfc/rfc6376.html>.
1360
1361 =item *
1362
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/>
1365
1366 =item *
1367
1368 L<nsupdate(1)>.
1369
1370 =back
1371
1372 =head1 AUTHOR
1373
1374 Mark Wooding, <mdw@distorted.org.uk>.
1375
1376 =cut
1377
1378 ###----- That's all, folks --------------------------------------------------