% dkim-rotate

PACKAGE STATUS
--------------

This package works well and even has some functional tests.
However, the documentation is not complete or well-organised.


CONFIGURATION
-------------

Config file is `/etc/dkim-rotate/`_instance_`.zone`
for any _instance_
(DNS hostname chracter set plus `_`, starting with a letter).
There may be multiple instances;
by default all will be processed.

Conventionally, the usual default instance is `/etc/dkim-rotate/dkim.conf`.

Each config is the fixed parts of a zonefile, minus the DKIM `TXT` RRs.

The string `;!SERIAL` must appear, immediately after some digits.
It should appear once, in the `SOA`.
The digits will be replaced by a suitable serial number,
and are used as the starting point for a new instance.

The file also contains special directives,
on lines starting with `!`,
which contain the configuration for dkim-rotate.
(These will be stripped out so that they don't appear
in the output zonefile gneerated by dkim-rotate.)

### `! max_selector` _selector_

Indicates number of `CNAME`s (selectors) to use.
_selector_ should be
a lowercase letter (indicating that `a` to _selector_ inclusive can be used)
or a positive integer (incidating that that many should be used).
(If this number is revised downwards after deployment,
it may take a little while for the extra names to stop being used.)

Default is `12` (or, equivalently, `l`).

### `! dns_lag` _duration_

Time after successful installation of new zonefile,
and nameserver reload,
after which the new DNS entries are expected to be visible to all clients.

_duration_ is a number (possibly with decimal fraction)
followed by one of the units `s` `m` `h` `d` `w`.

Default is `4h`.

### `! email_lag` _duration_

Time after successful installation of new MTA configuration,
and config reload,
after which we will assume that all messages
signed with the key indicated by the previous configration
have been delivered, or bounced.

Default is `88h`.

### `! instance_var_dir` _directory_

Where to store statefiles, MTA configuration, zone files, etc.
Default is `/var/lib/dkim-rotate/`_instance_
(ie derived from config leafname).

### `! dkim_txt` _tag list_

The values for the `TXT` RR, in domainkeys tag-list format,
not including the `p=`.
(see [RFC6376](https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1)).

dkim-rotate will append `; p=...` to this.
Default is `v=DKIM1; h=sha256; s=email`.

### `! rsa_bits` _integer_

Number of bits in generated RSA keys.
(Currently, only RSA is supported.)
Defaults to 2048.

### `! dns_reload` _shell command_

Command to run to reload the nameserver,
after the zonefile has been updated.
Default: `rndc reload >/dev/null`.

### `! mta_reload` _shell command_

Command to run to reload the MTA,
after the DKIM signing configuration has been updated.
Default: `true`.

### `! pub_url` _url-string_

URL at which our published private keys are available.
This should be a public URL
at which the directory _instance_var_dir_`/pub/` is accessible.

A warning will be generated if a supposedly-revealed key is found not
to be accessible.

`-` can be used to indicate "none".
This will also suppress the corresponding note
in the generated TXT RRs.

### `! mta_group` _identifier_

Group that ought to own `/var/lib/dkim-rotate/INSTANCE/priv`.
This should be the group of the MTA.

The permissions `0750` will be used
if dkim-rotate needs to create the diredtory.
(Otherwise it will just be checked that it doesn't have global execute.)

If `mta_group` is set to `-`,
dkim-rotate will skip the chgrp,
and (if need be) create the directory as `0700`.


MODEL
-----

### Example key lifecycle

 -- -------------  -------------- ---------------------------------------
    `-0200`        generate and
    [0][1]         advertise

                                   `dns_lag` (4h) + 2h slop

    `+0400`        start signing

                                   1d key rollover interval

    `+0400 +1d`    stop signing

                                   `email_lag`
                                   (3.5d timeout + 4h retry) + 2h slop

    `-0200 +5d`    deadvertise

                                   `dns_lag` (4h) + 2h slop

    `+0400 +5d`    reveal
 -- -------------  -------------- ---------------------------------------

[0]
`-0200` means "`2200` the previous day"

[1]
If a free selector is already available,
this might be generated and advertised at `+0400 -1d`.

### Example cron configuration

```
  #mins hrs	dom mon dow	command
  26 22	* * *		dkim-rotate --minor
  26 7	* * *		dkim-rotate --major
```

These jobs should be scheduled in a suitable local time
(in the timezone of the mail server's users),
because it is good for all mails sent on a particular calendar day
to become un-nonrepudiable (and un-deliverable) at once.

To cope nicely with timezone changes the interval
between `--minor` and the main run
should be at least `dns_lag` + 1h + an allowance for processing time etc.
The suggested configuration has a 6h interval,
which suits the default `dns_lag` of 4h.


### Key statuses and lifecycle, more formally

 -------- -------------------  ---------------- -------------- -------------
  abbrev  meaning              time_t           selector       how
                               (in                             many
                               statefile)                                
 -------- -------------------  ---------------- -------------- -------------
  `-1`    advertised;          first            advertised     0/1
          not yet used         advertised       (DNS)         
                                                                         
  `+0`    signing              invalid          advertised     0/1;
                               future           (DNS)          usually 1
                                                                         
  `+N`..  emails percolating   last used        advertised     [0 .. `sel_limit`]
                               for signing      (DNS)
                                                                         
  `+X`..  deadvertisment       last advertised  archival only  [0 ..];
          propagating                                          usually 0/1
                                                                         
  `R`     revealed             (not longer in   archival only  many
                               statefile)                          
 -------- -------------------  ---------------- -------------- --------------


STATE
-----

Lockfile `/var/lib/dkim-rotate/`_instance_`/lock`

### primary statefile `/var/lib/dkim-rotate/`_instance_`/state`

Updated with `rename(2)`.

Text file, line based, with the these keywords.
All of the following entries must appear, in this order:

#### `sel_offset` _selector_

Selector of first `key` entry.  (As a number.)

#### `sel_limit` _number_

Value that selectors are modulo.
Indicates the maximum selector that we are currently using,
or might use.

Normally this is equal to the `max_selector` config key.

#### `last_serial` _number_

Last DNS serial number used.
Each attempt to publish needs to increment this.

#### `status` _status_

Indicates that subsequent `key` entries have this status.
_status_ is one of 
`-1`
`+0`
`+N`
`+X`,
and, again, one of each `status` must appear, once, in order.
There must not be more than one key in `status -1` or `status +0`.

#### `key` _selector_ _time_t_ _keyname_ _dkim dns data_

Defines a key.
Repeated zero or more times after or in between `status`.

_selector_ must be the letter corresponding to
(key number in list + `sel_offset`) % `sel_limit`).

_time_t_ is in decimal.
It may also be one or more of
the two special values `DNS` or `MTA`,
separated by commas.
These means that the config update event referred to
has not yet been completed.

After all of these has been completed,
the value should be replaced with the time of the successful update.
Eg, `DNS` means "DNS zonefile update is outstanding";
the new DNS zone should be generated and the nameserver reloaded,
and then `DNS` replaced with the time of that update.

_keyname_ is derived from the public key value.

_dkim dns data_
(starting at the first nonwhitespace)
is the combined content of the `TXT` strings
tbat ought to be published in the DNS to advertise this key.

### Outputs for MTA and nameserver

#### `/var/lib/dkim-rotate/INSTANCE/zone`

Zonefile in standard master file syntax.
Created by taking the config file,
replacing `%SERIAL`,
and appending `TXT` RRs definitions.
The appeneded RRs have single-character alphabetic labels,
the selectors.

#### `/var/lib/dkim-rotate/INSTANCE/exim`

File in format suitable for exim `${lsearch }`.
Contents are:

```
selector: S
privkey: /var/lib/dkim-rotate/INSTANCE/priv/KEYNAME.pem
```

where `S` is the selector to use.

### Private key - private storage and public archive

```
/var/lib/dkim-rotate/INSTANCE/pub/README.txt
/var/lib/dkim-rotate/INSTANCE/pub/HH/KEYNAME.pem
/var/lib/dkim-rotate/INSTANCE/priv/KEYNAME.pem
```

Here _HH_ is the first two characters of _keyname_.

#### _keyname_

_keyname_ is derived from the public key value, as follows:

  1. base64 decode the `p=` base64string
  2. run the result through MD5.
  3. represent the result in hex

Eg, `base64 -d <contents-of-p-field | md5sum`.

Note that this is *not* sufficient to use as a key identifier
for verification purposes,
but it *is* nicely uniformly distributed
(so can't be pre-guessed)
and determinable from the public key
(so that if someone could verify a DKIM signature
they can know where to try to find the leaked key).

#### Permissions, visibility, and metadata

The directory `priv/` must be readable by the MTA,
but not otherwise accessible.
With Exim on Debian that probably means
it ought to be group-owned by `Debian-exim`.

The directories `pub/HH` should all *executable* but *unreadable*
by the webserver.
All of these directories should be created before any work is done.
This prevents enumeration of the keys.

The individual private key files in `pub/` will all have
the fixed mtime with time_t `1000000000`.
This prevents a putative verifier from determining when a key first existed.

`pub/README.txt` explains the situation
and is installed and maintained automatically.

ALGORITHM
---------

 0.
   Preparation/cleanup:
   * Check that `pub/` directories exist.
   * Update the README.txt.

 1.
   Check if last `+X` can be revealed.

   * Preconditions:
      * `dns_lag` elapsed.

   * If so:
      * Reveal: it becomes status `R`;
        * touch it to obliterate the mtime
        * install it in `pub/`
      * Repeat check for new last `+X`.

 2.
   Check if last +N can be deadvertised.

   * Preconditions:
      * `email_lag` elapsed.
   * If so:
      * Deadvertise, make into `+X`.
      * Repeat check for new last `+N`.

 3.
   Check if actual selector limit can be adjusted towards intended.
   Requirements:

    * No key's selector would change due to change of modulus.
    * Selectors being abolished are not actually used.
      (Used includes keys in state `+X`,
      so that seeing `sel_limit` decrease is suffiient to know
      one can change/remove the "upstream" DNS `CNAME`s.)

   This is fiddly.  Instead we use the following conditions:

    * `sel_offset` is zero.
    * The new limit is at least equal to the number of keys we have.

 4. 
    Possibly advance `-1` key to become newly-in-use `+0`.

    * Preconditions:
      * `dns_lag` (for `-1`) has elapsed.
      * We are not a "secondary" run.

  * If so:
     * Current `+0` becomes `+N`
     * Current `-1` becomes `+0`
     * MTA updated accordingly

 5.
   Possibly create `-1` key.

   * Preconditions:
     * No existing `-1` key.
     * The intended selector is available (conflict with `+X` is OK)
       (not used to publish another key).
     * The intended selector is within
       the config's `max_selector` (the intended `sel_limit`).
       Again, this condition is fiddely: instead, we check that
       the proposed new number of keys doesn't exceed `max_selector`.

  * If so:
     * Generate new key, and establish its _keyname_.

COPYRIGHT
---------

dkim-rotate is
Copyright 2022 Ian Jackson and contributors to dkim-rotate.
SPDX-License-Identifier: `GPL-3.0-or-later`.
There is **NO WARRANTY**.
