chiark / gitweb /
dkim-keys.in (generate_key): Set the permission bits explicitly.
[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   chmod 0640, $file;
560   umask $oldmask;
561 }
562
563 sub extract_public_key ($$) {
564   my ($pub, $priv) = @_;
565   ## Store the public key corresponding to PRIV in the file PUB.
566   ##
567   ## No particular effort is taken to ensure that PUB is updated atomically.
568
569   print "extract public key\n";
570   run "openssl", "rsa", "-pubout", "-in", $priv, "-out", $pub;
571   return read_pem($pub, "PUBLIC KEY");
572 }
573
574 { package Key;
575   ## A `Key' object keeps track of a DKIM key's lifecycle stages.
576   ##
577   ## Key objects are blessed hashrefs with the following public slots.
578   ##
579   ##   * `id' is the key's identifier, as a lowercase Base32 string.
580   ##   * `st' is the key's state, as an uppercase string.
581   ##   * `t' is the key's timestamp, as an integer count of seconds.
582   ##
583   ## It also has private slots.
584   ##
585   ##   * `file0' is the filename holding the private key.
586   ##   * `file' is the filename that the private key /would/ have if the
587   ##     currently scheduled operations were executed.
588   ##   * `pub' is the key's public key, in binary BER format.
589
590   ## External modules.
591   use autodie;
592   use strict;
593
594   use Carp;
595   use MIME::Base64;
596
597   sub publish_components {
598     my ($me) = @_;
599     ## Split the key ID into pieces.  This is how we map key IDs into the
600     ## filesystem and URI space: if we return n pieces, that's n - 1 levels
601     ## of directory, with leaves at the bottom.
602
603     my $id = $me->{id};
604     return (substr($id, 0, 3), substr($id, 3, 5), substr($id, 8));
605   }
606
607   sub publish_uri {
608     my ($me) = @_;
609     ## Return the URI at which the key will be published.
610
611     my @cc = $me->publish_components;
612     return $::C{"publish-uri"} . join("/", @cc) . ".html";
613   }
614
615   sub publish_filename {
616     my ($me) = @_;
617     ## Return the filename at which the key publication HTML file is stored.
618
619     my @cc = $me->publish_components;
620     my $name = "publish";
621     for my $c (@cc) { ::maybe_mkdir $name, 0751; $name .= "/" . $c; }
622     return $name . ".html";
623   }
624
625   sub new {
626     my ($pkg, $t) = @_;
627     ## Return a new `Key' object with initial timestamp T.
628     ##
629     ## The `pub' slot is set on exit.
630
631     ## Make the new key.
632     ::maybe_mkdir "NEW", 0700;
633     ::generate_key "NEW/new.priv";
634
635     ## Extract the public key and determine its id.
636     my $pub = ::extract_public_key "NEW/new.pub", "NEW/new.priv";
637     my $id = ::ident $pub;
638
639     ## Rename the private key.  It won't go away now: we're committed to this
640     ## one.
641     print "commit to new key $id\n";
642     my $file = sprintf "NEW/%s.%s.priv", ::present_stamp($t), $id;
643     rename "NEW/new.priv", $file;
644
645     ## Report the public key as active.
646     print "activated new public key $id\n";
647     ::maybe_mkdir "active";
648     rename "NEW/new.pub", "active/$id.pub";
649
650     ## It's now a proper key.
651     my $me = $pkg->load($file);
652     $me->{pub} = $pub;
653     return $me;
654   }
655
656   sub load {
657     my ($pkg, $file) = @_;
658     ## Return a key object for the given FILE.  We probably don't actually
659     ## bother reading the file.
660
661     ## Check that the file is sane.
662     -r $file or confess "key file `$file' not found";
663     $file =~ m{^ (\w+) /
664                  (\d{4} - \d{2} - \d{2} T \d{2} : \d{2} : \d{2} Z) \.
665                  ([abcdefghijklmnopqrstuvwxyz234567]+) \.priv $}x
666                    or confess "bad key name `$file'";
667
668     ## Build the object.as the primary
669     my $st = $1; my $tm = $2; my $id = $3;
670     return bless {
671       st => $st, id => $id, t => ::parse_stamp($tm),
672       file => $file, file0 => $file
673     }, $pkg;
674   }
675
676   sub set_state {
677     my ($me, $sched, $newst, $newtime) = @_;
678     ## Change the state of the key to be NEWST, with timestamp NEWTIME.
679     ## Arrange to commit this state change by renaming the private key to
680     ## `ST/TS.ID.priv'.
681
682     return if $newst eq $me->{st} && $newtime eq $me->{t};
683     my $id = $me->{id};
684     printf "prepare key %s state %s %s -> %s %s\n",
685       $id, $me->{st},
686       ::present_time($me->{t}), $newst, ::present_time($newtime);
687     my $from = $me->{file};
688     my $to = $newst . "/" . ::present_stamp($newtime) . "." . $id . ".priv";
689     ::maybe_mkdir $newst;
690     ::schedule_cleanup {
691       print "key $id rename $from -> $to\n";
692       rename $from, $to;
693     } $sched;
694     $me->{st} = $newst; $me->{t} = $newtime; $me->{file} = $to;
695   }
696
697   sub announce {
698     my ($me, $sched, $t_publish) = @_;
699     ## Announce the public key in DNS as `ID.ZONE', and create a placeholder
700     ## HTML file in `publish/I/D.html' explaining that the key will be
701     ## published before T_PUBLISH.
702
703     ## Initial preparation.
704     print "announce new key\n";
705     my $id = $me->{id};
706
707     ## If we don't already have the public key then this must be a leftover
708     ## key from a previous aborted run.  Create the public key file and
709     ## retrieve the key.  Either way, we don't need it any more after this,
710     ## so save the memory.
711     my $pub = $me->{pub}; delete $me->{pub};
712     unless (defined $pub) {
713       ::maybe_mkdir "active";
714       $pub = ::extract_public_key "active/$id.pub", $me->{file0};
715     }
716
717     ## Prepare the record data.
718     my $key = sprintf
719       "v=DKIM1; s=email; t=s:y; k=rsa; h=sha256; " .
720       "n=Not suitable for non-repudiation!  " .
721         "Private key revealed at %s by %s; " .
722       "p=%s",
723       $me->publish_uri,
724       ::present_time($t_publish),
725       encode_base64($pub, "");
726
727     ## A TXT record data consists of a sequence of strings of length at most
728     ## 255 each.  The split positions are not significant.  Split the data
729     ## into sufficiently small pieces, preferring to split at semantic
730     ## boundaries.
731     my @key = ();
732     while (length $key > 255) {
733       my $chunk = substr $key, 0, 255, "";
734       if ($chunk =~ m{^ (.* \;\s+) ([^\s;] [^;]*) $}x)
735         { $chunk = $1; $key = $2 . $key; }
736       push @key, $chunk;
737     }
738     push @key, $key;
739     $key = join " ", map qq'"$_"', @key;
740
741     ## Add the record.
742     $sched->add_record($id . "." . $::C{"ddns-zone"}, "TXT", $key);
743   }
744
745   sub deploy {
746     my ($me, $sched) = @_;
747     ## Make the key suitable for deployment.  Hard link the private key to
748     ## `active/ID.priv'.
749
750     my $id = $me->{id};
751     print "deploy private key $id\n";
752     ::maybe_mkdir "active";
753     ::maybe_unlink "active/$id.priv.new";
754     link $me->{file0}, "active/$id.priv.new";
755     rename "active/$id.priv.new", "active/$id.priv";
756   }
757
758   sub withdraw {
759     my ($me, $sched) = @_;
760     ## Withdraw the key data from DNS so that legitimate reliers won't
761     ## believe signatures any more.
762
763     my $id = $me->{id}; my $zone = $::C{"ddns-zone"};
764     print "withdraw key $id from dns\n";
765     $sched->delete_record("$id.$zone", "TXT");
766   }
767
768   sub delete {
769     my ($me, $sched) = @_;
770     ## Delete the key from everywhere except the publication tree.
771
772     my $id = $me->{id}; print "delete key $id\n";
773     ::schedule_cleanup {
774       print "delete published key $id\n";
775       ::maybe_unlink "active/$id.pub";
776       ::maybe_unlink "active/$id.priv";
777       unlink $me->{file};
778     } $sched;
779   }
780 }
781
782 ###--------------------------------------------------------------------------
783 ### Publication archive.
784
785 sub publication_time ($;$$) {
786   my ($k, $newst, $newtime) = @_;
787   ## Return the approximate publication time of key K.  If NEWST and NEWTIME
788   ## are given, then they override the key's state and timestamp.
789
790   $newst //= $k->{st}; $newtime //= $k->{t};
791   defined (my $newix = $STIX{$newst})
792     or confess "unexpected state `$newst'";
793   my $t_publish = $newtime + $C{"cycle-period"};
794   if ($newix < $STIX{PUBLISH})
795     { $t_publish += $C{"dns-persistence"}; }
796   if ($newix < $STIX{WITHDRAW})
797     { $t_publish += $C{"mail-persistence"}; }
798   if ($newix < $STIX{RETIRE})
799     { $t_publish += $C{"active-duration"} + $C{"cycle-period"}; }
800   return $t_publish;
801 }
802
803 sub publish_placeholder ($;$$) {
804   my ($k, $newst, $newtime) = @_;
805   ## Write a placeholder HTML file at the URI that key K's private key will
806   ## be published later.  If NEWST and NEWTIME are given, then the override
807   ## the key's state and timestamp in the computation of the publication
808   ## time.
809
810   ## Determine the things we need to know.
811   my $id = $k->{id}; my $inst = $C{"instance"};
812   my $file = $k->publish_filename;
813   my $t_publish = publication_time $k, $newst, $newtime;
814   my $th_publish = present_time $t_publish;
815
816   ## Produce the placeholder file.
817   print "publish placeholder for $id\n";
818   open my $f, ">", "$file.new";
819   print $f <<EOF;
820 <!DOCTYPE html>
821 <html>
822 <head>
823   <title>$inst DKIM key $id</title>
824 </head>
825 <body>
826 <h1>$inst DKIM key <code>$id</code></h1>
827 <p>This is a placeholder page.
828 The private key <code>$id</code> is scheduled for publication
829 on or before $th_publish.
830 $inst DKIM private keys are published in order to make them
831 unsuitable as evidence after the fact.
832
833 <p>The public key is
834 <pre>
835 EOF
836   dump_file $f, "active/$id.pub";
837   print $f <<EOF;
838 </pre>
839 </body>
840 </html>
841 EOF
842   close $f;
843   rename "$file.new", $file;
844 }
845
846 sub publish_key ($) {
847   my ($k) = @_;
848   ## Publish key K's private key.
849
850   ## Determine the things we need to know.
851   my $id = $k->{id}; my $inst = $C{"instance"};
852   my $file = $k->publish_filename;
853
854   ## Produce the publication file.
855   print "publish private key for $id\n";
856   open my $f, ">", "$file.new";
857   print $f <<EOF;
858 <!DOCTYPE html>
859 <html>
860 <head>
861   <title>$inst DKIM key $id</title>
862 </head>
863 <body>
864 <h1>$inst DKIM key <code>$id</code></h1>
865 <p>The key <code>$id</code> was used as a key to authenticate
866 that email passed through the $inst server.
867 $inst DKIM keys are published after they are no longer in active use,
868 to make them unsuitable as evidence after the fact.
869
870 <p>The private and public keys are
871 <pre>
872 EOF
873   dump_file $f, "active/$id.priv";
874   dump_file $f, "active/$id.pub";
875   print $f <<EOF;
876 </pre>
877 </body>
878 </html>
879 EOF
880   close $f;
881   rename "$file.new", $file;
882 }
883
884 ###--------------------------------------------------------------------------
885 ### State transitions.
886
887 sub set_key_state ($$$$) {
888   my ($k, $sched, $newst, $newtime) = @_;
889   ## Advance K to state NEWST, and set its timestamp to NEWTIME.  Arrange for
890   ## SCHED to complete the transition.
891
892   ## Preliminary checking.
893   my $oldst = $k->{st}; my $id = $k->{id};
894   defined (my $ix = $STIX{$oldst})
895     or confess "unexpected key state `$oldst'";
896   defined (my $newix = $STIX{$newst})
897     or confess "unexpected new state `$newst'";
898   $ix <= $newix
899     or confess "attempted state regression `$oldst' -> `$newst'";
900
901   ## Advance the key through each intermediate state in turn.
902   while ($ix < $newix) {
903     $ix++; my $curst = $STNAME[$ix];
904     print "advance key $id state $oldst -> $curst\n";
905     if ($curst eq "ANNOUNCE") {
906       $k->announce($sched, publication_time $k, $newst, $newtime);
907       publish_placeholder $k, $newst, $newtime;
908     }
909     elsif ($curst eq "DEPLOY") { $k->deploy($sched); }
910     elsif ($curst eq "RETIRE") { ; }
911     elsif ($curst eq "WITHDRAW") { $k->withdraw($sched); }
912     elsif ($curst eq "PUBLISH") { publish_key $k; }
913     else { confess "unexpected state $curst" }
914   }
915
916   ## Finally commit the key in its new state.
917   if ($newst eq "PUBLISH") { $k->delete($sched); }
918   else { $k->set_state($sched, $newst, $newtime); }
919 }
920
921 sub keys_in_state ($) {
922   my ($st) = @_;
923   ## Return a list of the keys in state ST.
924
925   my @k; my $d;
926   eval { opendir $d, $st; };
927   if ($@) {
928     die if $@->errno != ENOENT;
929   } else {
930     FILE: for my $f (readdir $d) {
931       next FILE if $f eq "." || $f eq "..";
932       next FILE if $st eq "NEW" && ($f eq "new.priv" || $f eq "new.pub");
933       push @k, Key->load("$st/$f");
934     }
935   }
936   return @k;
937 }
938
939 ###--------------------------------------------------------------------------
940 ### Main program.
941
942 ## Our first order of business is to ensure that there are keys lined up for
943 ## the current deployment cycle.  The candidates are the keys which are
944 ## currently in `NEW', `ANNOUNCE', and `DEPLOY' states.  We arrange to use
945 ## these in ascending order of their current timestamps.  This might cause
946 ## keys to become active earlier than before.
947
948 my @candidates;                         # candidates for future deployment
949 my @to_retire;                          # keys which are too old
950 my $cutoff = $NOW - $C{"active-duration"}; # threshold between the two
951 my $sched = Scheduler->new;             # a `Scheduler' instance
952
953 ## Work through the `NEW' keys and advance them on to `ANNOUNCE'.  Pick up
954 ## all of the unretired keys so that we can plan how to use them.
955
956 sub notice_candidate ($) {
957   my ($k) = @_;
958   ## Notice a key and add it to one of the lists above.  If K's deployment
959   ## time window is entirely in the past then add it to `@to_retire';
960   ## otherwise, add it to `@candidates'.
961
962   if ($k->{t} < $cutoff) { push @to_retire, $k; }
963   else { push @candidates, $k; }
964 }
965
966 for my $k (keys_in_state "NEW")
967   { $k->announce($sched, publication_time $k); notice_candidate $k; }
968 for my $st (qw{ ANNOUNCE DEPLOY })
969   { for my $k (keys_in_state $st) { notice_candidate $k; } }
970 @candidates = sort { $a->{t} <=> $b->{t} } @candidates;
971
972 ## Determine the current start time of the oldest key we can deploy.  If this
973 ## is earlier than now then things are chugging along OK and we continue with
974 ## the current plan.  Otherwise, we need to arrange keys for immediate use.
975 my $t0 = @candidates && $candidates[0]{t} <= $NOW ? $candidates[0]{t} : $NOW;
976 my $nk = 0;
977
978 ## Start producing the state file for the mail server.  Make sure that the
979 ## current cycle is covered.
980 my $info = "";
981 my $t = $t0; my $t_limit = $NOW + $C{"cycle-period"};
982 while ($t < $t_limit) {
983   my $k = @candidates ? shift @candidates : Key->new($t);
984   set_key_state $k, $sched, "DEPLOY", $t; $t += $C{"active-duration"};
985   $info .= sprintf "info.%d: k = %s u = %s tpub = \"%s\"\n",
986     $nk, $k->{id}, $k->publish_uri, present_time publication_time $k;
987   $nk++;
988 }
989
990 ## Write the new state file.
991 maybe_mkdir "active";
992 open my $mcf, ">", "active/dkim-keys.state.new";
993 printf $mcf "### dkim-keys deployment state, from %s up to %s\n\n",
994   present_time($t0), present_time($t);
995 printf $mcf "params: t0 = %d step = %d n = %d\n",
996   $t0, $C{"active-duration"}, $nk;
997 print $mcf $info;
998 close $mcf; rename "active/dkim-keys.state.new", "active/dkim-keys.state";
999
1000 ## And now make sure there are enough new keys announced to cover the next
1001 ## cycle.
1002 $t_limit = $NOW + 2*$C{"cycle-period"};
1003 while ($t < $t_limit) {
1004   my $k = @candidates ? shift @candidates : Key->new($t);
1005   set_key_state $k, $sched, "ANNOUNCE", $t; $t += $C{"active-duration"};
1006 }
1007
1008 ## If there are keys to retire, then arrange that.
1009 for my $k (@to_retire) {
1010   set_key_state $k, $sched, "RETIRE",
1011     $k->{t} + $C{"active-duration"} + $C{"mail-persistence"};
1012 }
1013
1014 ## If there are keys to withdraw, then arrange that too.
1015 for my $k (keys_in_state "RETIRE") {
1016   if ($k->{t} <= $NOW)
1017     { set_key_state $k, $sched, "WITHDRAW", $NOW + $C{"dns-persistence"}; }
1018 }
1019
1020 ## And, finally, if there are keys to be published, then arrange that.
1021 for my $k (keys_in_state "WITHDRAW")
1022   { if ($k->{t} <= $NOW) { set_key_state $k, $sched, "PUBLISH", -1; } }
1023
1024 ## The planning is done.  It's now time to do all the things.
1025 print "running cleanup actions\n";
1026 $sched->execute;
1027
1028 ###--------------------------------------------------------------------------
1029 ### Manual.
1030
1031 =head1 NAME
1032
1033 dkim-keys - manage short-lived DKIM keys
1034
1035 =head1 SYNOPSIS
1036
1037 B<dkim-keys>
1038
1039 =head1 DESCRIPTION
1040
1041 =head2 Background
1042
1043 DKIM , RFC6376, is a mechanism for authenticating email messages.  An
1044 originating mail server signs each message that it sends using a private
1045 signing key, and adds a header to the message containing the signature and a
1046 reference to where the public verification key can be found in the DNS.
1047 A receiving mail server can parse the header, retrieve the verification key,
1048 verify the signature, and be convinced that the message at least passed
1049 through the originating server.  This is intended to reduce the effectiveness
1050 of forged email messages for fraud and spam.
1051
1052 A DKIM signature must cover email headers I<and> the body, to prevent an
1053 adversary from altering them in order to construct a forgery.  This creates
1054 a problem which the designers of DKIM failed to foresee.  Messages bearing
1055 DKIM signatures can remain verifiable long after delivery, providing
1056 convincing evidence that a particular mail server transmitted a particular
1057 message, and, moreover, at a particular time, and that was sent by a
1058 particular user.  This may be undesirable, to say the least.
1059
1060 In 2020, Matthew Green wrote an article describing this problem on his
1061 I<Cryptography Engineering> blog, and suggesting a solution: that DKIM
1062 signing keys should cycle rapidly, each key being used only for a short time,
1063 say a day, and, once all messages bearing signatures from a particular key
1064 have been delivered or abandoned, the I<private> signing key should be
1065 published.  Once this is done, the DKIM signature on an old message becomes
1066 worthless as evidence of anything, since anyone with a little technical
1067 ability can forge convincing-looking messages bearing a valid signature from
1068 the key.
1069
1070 This is what B<dkim-keys> does.
1071
1072 =head2 Inovking B<dkim-keys>
1073
1074 The B<dkim-keys> doesn't usually take any command-line arguments.  It does
1075 recognize B<--help> (B<-h>) and B<--version> (B<-v>) options, for
1076 consistency's sake.  It reports an error if other command-line arguments are
1077 given.
1078
1079 =head2 Operation
1080
1081 The B<dkim-keys> program works in the current directory.  It expects to find
1082 a configuration file B<dkim-keys.conf>.  It will create directories for its
1083 own use as necessary.
1084
1085 =head2 State model
1086
1087 Each key has an I<identifier>, notated I<id> here.  For RSA keys, which are
1088 the only kind currently implemented, this is the least significant 80 bits of
1089 the modulus, encoded in lowercase Base32.  (This means that the identifier
1090 can easily be determined from just the public key.)  There are no padding
1091 characters because 80 is a multiple of five.  The key identifier is used in
1092 the DKIM selector when the key is in use.
1093
1094 Keys advance through six states.
1095
1096 =over
1097
1098 =item *
1099
1100 A key in the B<NEW> state has just been created, and nobody else knows
1101 anything about it.  Under normal circumstances, keys immediately advance to
1102 B<ANNOUNCE>, but the B<NEW> state is separate for technical reasons.
1103
1104 =item *
1105
1106 A key in the B<ANNOUNCE> state has a record listed in the DNS, and we're
1107 waiting for the DNS records to propagate before the key can be used.
1108
1109 =item *
1110
1111 A key in the B<DEPLOY> state is ready for use by a mail server to sign
1112 outgoing messages.  There are usually multiple keys in this state so that the
1113 mail server can cycle from one to the next at the appropriate time without
1114 any further action from B<dkim-keys>.
1115
1116 =item *
1117
1118 A key in the B<RETIRE> state has completed its time in service.  It may have
1119 been used to sign outgoing messages which are still on their way to being
1120 delivered or abandoned, so legitimate verifiers need access to the public
1121 key, but it won't be used to sign new messages.
1122
1123 =item *
1124
1125 A key in the B<WITHDRAW> state should no longer be of use to receiving
1126 servers.  All messages signed using the key have either been delivered or
1127 abandoned.  The public key record is withdrawn from the DNS, and we're
1128 waiting for this withdrawal to propagate before the private key can be
1129 published.  At this stage, a forgery made using the key might be accepted by
1130 a receiving server which still has access to stale DNS records, so the
1131 private key can't quite be published yet.
1132
1133 =item *
1134
1135 A key in the B<PUBLISH> state is published for all to see.  Nobody should
1136 beleive it for anything, and nobody should have any reason to.
1137
1138 =back
1139
1140 =head2 Configuration file
1141
1142 The configuration syntax is simple and line-based.  Lines consisting only of
1143 whitespace, and lines whose first whitespace character is C<#>, are ignored.
1144 Other lines must be I<assignments> of the form I<key> B<=> I<value>.  The
1145 recognized keys and their values are as follows.
1146
1147 =over
1148
1149 =item B<instance>
1150
1151 A name for the instance of B<dkim-keys>.  This is used in the published HTML
1152 files.  There is no default.
1153
1154 =item B<publish-uri>
1155
1156 The base URL at which keys will be published.  The web server should be
1157 configured to publish the contents of the B<publish> directory at this URL.
1158 There is no default.
1159
1160 =item B<ddns-zone>
1161
1162 The DNS I<zone> in which the public keys will be listed.  A key with a given
1163 I<id> will be listed in a B<TXT> record at I<id>B<.>I<zone>.  In a simple
1164 system, I<zone> might be B<_domainkey.example.org>, so the B<example.org>
1165 outgoing mail server need only set B<s=>I<id>B<;> B<d=example.org> in its
1166 DKIM headers.  In a more complex case, the I<zone> might be referred to using
1167 B<DNAME> for multiple domains.
1168
1169 There is no default for this setting.
1170
1171 =item B<ddns-server>
1172
1173 The server to which DNS updates will be submitted.  By default, the source
1174 server listed in the zone B<SOA> record will be used.
1175
1176 =item B<ddns-key>
1177
1178 The file containing the TKIP key to use for authenticating the DNS update.
1179 There is no default for this setting.
1180
1181 =item B<ddns-ttl>
1182
1183 The TTL to set on DKIM key records, as a duration (see below).  The default
1184 is four hours.
1185
1186 =item B<dns-delay>
1187
1188 The time required for a newly published DNS record to fully propagate, as a
1189 duration (see below).  Conservatively, this will be the sum of the zone
1190 refresh, expiry, and minimum-TTL times: a secondary server must have time to
1191 try updating, give up, and expire the zone, and a downstream recursive
1192 resolver must time out a previously cached negative result for the record,
1193 before the record can be considered fully propagated.
1194
1195 This setting determines how long a key remains in the B<ANNOUNCE> state.  The
1196 default is two days.
1197
1198 =item B<active-duration>
1199
1200 The time for which a key will be in active use signing outgoing messages, as
1201 a duration (see below).  The default is one day.
1202
1203 =item B<cycle-period>
1204
1205 The maximum interval between runs of B<dkim-keys>, as a duration (see below).
1206 This determines the how many keys are queued up in the B<DEPLOY> state.  The
1207 default is three days, with the expectation that the script will actually run
1208 daily.
1209
1210 =item B<mail-persistence>
1211
1212 The time before which outgoing email messages can safely be expected to have
1213 been delivered or abandoned, as a duration (see below).  This mostly depends
1214 on the outgoing server configuration, but there can conceivably be additional
1215 delay at the receiving end.
1216
1217 This setting determines how long a key remains in the B<RETIRE> state.  The
1218 default is one week.
1219
1220 =item B<dns-persistence>
1221
1222 The time required for a withdrawn DNS record to fully propagate, as a
1223 duration (see below).  Conservatively, this will be the sum of the zone
1224 refresh and expiry times, and the record TTL: a secondary server must have
1225 time to try updating, give up, and expire the zone, and a downstream
1226 recursive resolver must time out a previously cached record, before it can be
1227 considered fully withdrawn.
1228
1229 This setting determines how long a key remains in the B<WITHDRAW> state.  The
1230 default is three days.
1231
1232 =back
1233
1234 A duration is a (possibly fractional) decimal number, followed by an optional
1235 unit.
1236
1237 =over
1238
1239 =item *
1240
1241 B<s>, B<sec>, B<secs>, B<second>, B<seconds>
1242
1243 =item *
1244
1245 B<min>, B<m>, B<mins>, B<minute>, B<minutes>
1246
1247 =item *
1248
1249 B<hr>, B<h>, B<hrs>, B<hour>, B<hours>
1250
1251 =item *
1252
1253 B<dy>, B<d>, B<dys>, B<day>, B<days>
1254
1255 =item *
1256
1257 B<wk>, B<w>, B<wks>, B<week>, B<weeks>
1258
1259 =back
1260
1261 If no unit is given, the default is seconds.
1262
1263 =head2 Output
1264
1265 The B<active> directory contains public and private key files, and a
1266 B<dkim-keys.state> file for the mail server.
1267
1268 B<active/>I<id>B<.pub> contains the public key for the key with identifier
1269 I<id> (in OpenSSL PEM format), and B<active/>I<id>B<.priv> is the private key
1270 (a hard link to the I<state>B</>I<timestamp>B<.>I<id>B<.priv> file).
1271
1272 The B<active/dkim-keys.state> file is formatted as follows.  It contains
1273 blank lines and (sparse) comments beginning with C<#>.  It contains a line
1274
1275 =over
1276
1277 B<params:> B<t0 => I<t0> B<step => I<step> B<n => I<n>
1278
1279 =back
1280
1281 where I<t0>, I<step>, and I<n> are integers in decimal notation.  It also
1282 contains lines
1283
1284 =over
1285
1286 B<info.>I<i>B<:> B<k => I<id> B<u => I<url> B<tpub => I<tpub>
1287
1288 =back
1289
1290 where I<i> is an integer, I<id> is a key identifier, I<url> is a url (not
1291 containing spaces), and I<tpub> is an ISO8601 time stamp with explicit time
1292 zone offset, in double quotes.  There will be exactly I<n> B<info> lines, one
1293 for each I<i> from 0 (inclusive) up to but not including I<n>.  These lines
1294 may appear in any order.
1295
1296 The B<params> line identifies one of the B<info> lines, as follows.  I<t0> is
1297 a POSIX timestamp, counting nonleap seconds since the start of January 1970.
1298 I<step> is the length of time, in seconds, for which a key is active (taken
1299 from the B<active-duration> configuration setting).  If the current POSIX
1300 time is I<t>, then the mail server should use the B<info.>I<i> data, where
1301 I<i> = floor((I<t> - I<t0>)/I<step>).  If I<i>, as computed in this way, is
1302 less than zero or greater than or equal to I<n>, then the data is invalid:
1303 the mail server should report temporary failure.  Specifically, if I<i> is
1304 less than zero then the data is apparently from the future: this is most
1305 likely if the system clock has been stepped, and the situation can be
1306 corrected by running B<dkim-keys> again.  If I<i> is greater than or equal to
1307 I<n> then the file is out-of-date: B<dkim-keys> has not run to completion
1308 sufficiently recently, or is otherwise broken.
1309
1310 The B<info> line fields are as follows.  The I<id> is the identifier of the
1311 signing key to use, and to include in the DKIM B<s=> selector tag; the actual
1312 private key will be B<active/>I<id>B<.priv>.  The I<url> is the URL at which
1313 the private key will be published (derived from the key identifier and the
1314 B<publish-uri> configuration setting) and I<tpub> is a time by which
1315 publication should have occurred; the latter two items are intended to be
1316 included in a message header.
1317
1318 Exim, for example, might be configured as follows.
1319
1320         dkim_selector = \
1321           ${lookup {params} lsearch \
1322                   {${lookup {${domain:$h_From:}} partial0-lsearch \
1323                           {/etc/mail/dkim-sign.conf} \
1324                     {/var/lib/dkim-keys/$value/active/dkim-keys.state}}} \
1325             {${if and {{>= {$tod_epoch} {${extract {t0}{$value}}}} \
1326                        {< {$tod_epoch} \
1327                           {${eval:${extract {t0}{$value}} + \
1328                                   ${extract {n}{$value}}*${extract {step}{$value}}}}}} \
1329               {${lookup {info.${eval:($tod_epoch - ${extract {t0}{$value}})/ \
1330                                      ${extract {step}{$value}}}}
1331                       lsearch \
1332                       {${lookup {${domain:$h_From:}} partial0-lsearch \
1333                               {/etc/mail/dkim-sign.conf} \
1334                         {/var/lib/dkim-keys/$value/active/dkim-keys.state}}} \
1335                 {${extract {k}{$value}}}fail}} \
1336               fail}}\
1337             fail}
1338         dkim_private_key = \
1339           ${lookup {${domain:$h_From:}} partial0-lsearch \
1340                   {/etc/mail/dkim-sign.conf} \
1341             /var/lib/dkim-keys/$value/active/$dkim_selector.priv}
1342
1343 =head2 Working state
1344
1345 In the working directory, there is a subdirectory for each state.  Private
1346 keys are stored (in OpenSSL PEM) format) in files named
1347 I<state>B</>I<timestamp>B<.>I<id>B<.priv> where I<state> is the key's state,
1348 as listed above, in uppercase, I<timestamp> is a timestamp in ISO8601 `zulu'
1349 format, and I<id> is the key's identifier, used as the DKIM selector.  The
1350 meaning and value of the timestamp changes as the key advances from one state
1351 to the next: see the source code for details.
1352
1353 =head1 SEE ALSO
1354
1355 =over
1356
1357 =item *
1358
1359 D. Crocker, T. Hansen, M. Kucherawy, RFC6376: I<DomainKeys Identified Mail
1360 (DKIM) Signatures>, L<https://www.rfc-editor.org/rfc/rfc6376.html>.
1361
1362 =item *
1363
1364 Matthew Green, I<Ok Google: Please publish your DKIM secret keys>,
1365 L<https://blog.cryptographyengineering.com/2020/11/16/ok-google-please-publish-your-dkim-secret-keys/>
1366
1367 =item *
1368
1369 L<nsupdate(1)>.
1370
1371 =back
1372
1373 =head1 AUTHOR
1374
1375 Mark Wooding, <mdw@distorted.org.uk>.
1376
1377 =cut
1378
1379 ###----- That's all, folks --------------------------------------------------