#!/bin/sh
# danectl - DNSSEC DANE implementation manager
# https://raf.org/danectl
# https://github.com/raforg/danectl
# https://codeberg.org/raforg/danectl
#
# Copyright (C) 2021-2023 raf <raf@raf.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <https://www.gnu.org/licenses/>.
#
# 20230412 raf <raf@raf.org>

name=danectl
version=0.8.1
date=20230412
author="raf <raf@raf.org>"
url=https://raf.org/danectl
git=https://github.com/raforg/danectl
git2=https://codeberg.org/raforg/danectl

# Some defaults

test=0
verbose=0
quiet=
oneline=0
spaces=0
le=/etc/letsencrypt
default_danectlrc="$HOME/.danectlrc"
danectlrc="$default_danectlrc"
umask 022

# The --help usage message

usage()
{
	rc="$1"; shift
	[ -z "$*" ] || echo "$name: $*"
	cat << USAGE
usage: $name [options] command [arg...]

options:
 -h, --help                              - Show the usage message
 -V, --version                           - Show the name and version
 -q, --quiet                             - Quiet mode (pass -q to certbot)
 -v, --verbose                           - Verbose mode (announce actions)
 -n, --test                              - Test mode (don't perform actions)
 -g, --group groupname                   - Refer to a subgroup of certificates
 -1, --oneline                           - Output long RRs on a single line
 -s, --spaces                            - Include spaces in --oneline output

commands:
 help                                    - Show the manual page
 aliases                                 - Show the command aliases

 certbot [run|certonly] <options...>     - Record certbot auth/install details
 adopt <certname>                        - Adopt an existing cert as current
 new <domain...>                         - Create a new original/current cert
 dup <domain...>                         - Create a new duplicate/next cert
 add-tlsa <certname> <_#._tcp[.host]...> - Add port/pro/host for TLSA output
 del-tlsa <certname> <_#._tcp[.host]...> - Delete port/pro/host for TLSA output
 show-tlsa <certname>                    - Show port/pro/host for TLSA output
 tlsa-current <certname...>              - Output TLSA RRs for the current key
 tlsa-next <certname...>                 - Output TLSA RRs for the next key
 tlsa-check [<certname...>]              - Check that TLSA RRs are published
 add-reload <certname> <service...>      - Add services to reload on rollover
 del-reload <certname> <service...>      - Delete services to reload on rollover
 show-reload <certname>                  - Show services to reload on rollover
 reload <certname...>                    - Reload services affected by rollover
 rollover <certname...>                  - Perform a key rollover
 status                                  - Show current/next status

 sshfp <domain>                          - Output SSHFP RRs for localhost
 sshfp-check <domain>                    - Check that SSHFP RRs are published
 openpgpkey <emailaddress>               - Output OPENPGPKEY RR for an email
 openpgpkey-check <emailaddress>         - Check OPENPGPKEY RR is published
 smimea <smimecert.pem>                  - Output SMIMEA RR for a certificate
 smimea-check <smimecert.pem>            - Check SMIMEA RR is published

Danectl is a DNSSEC DANE implementation manager. It uses certbot(1) to create
and manage pairs of keys for use with a TLSA 3 1 1 current + next workflow.
It generates TLSA records for your TLS services for you to publish in the
DNS, checks that they are correctly published, and performs key rollovers.

Danectl can also generate and check SSHFP records for the local SSH server.
Danectl can also generate and check an OPENPGPKEY record for a GnuPG key.
Danectl can also generate and check an SMIMEA record for an S/MIME certificate.

Read "danectl help" for more information.

Name: $name
Version: $version
Date: $date
Author: $author
URL: $url
GIT: $git
GIT: $git2

Copyright (C) 2021-2023 $author

This is free software released under the terms of the GPLv2+:

  https://www.gnu.org/licenses

There is no warranty; not even for merchantability or fitness
for a particular purpose.

Report bugs to $author
USAGE
	exit "$rc"
}

# The long help message

help()
{
	cat << HELP
NAME

danectl - DNSSEC DANE implementation manager

SYNOPSIS

 usage: $name [options] command [arg...]

 options:
  -h, --help                             - Show the usage message
  -V, --version                          - Show the name and version
  -q, --quiet                            - Quiet mode (pass -q to certbot)
  -v, --verbose                          - Verbose mode (announce actions)
  -n, --test                             - Test mode (don't perform actions)
  -g, --group groupname                  - Refer to a subgroup of certificates
  -1, --oneline                          - Output long RRs on a single line
  -s, --spaces                           - Include spaces in --oneline output

 commands:
  help                                   - Show the manual page
  aliases                                - Show the command aliases

  certbot [run|certonly] <options...>    - Record certbot auth/install details
  adopt <certname>                       - Adopt an existing cert as current
  new <domain...>                        - Create a new original/current cert
  dup <domain...>                        - Create a new duplicate/next cert
  add-tlsa <certname> <_#._tcp[.host]..> - Add port/pro/host for TLSA output
  del-tlsa <certname> <_#._tcp[.host]..> - Delete port/pro/host for TLSA output
  show-tlsa <certname>                   - Show port/pro/host for TLSA output
  tlsa-current <certname...>             - Output TLSA RRs for the current key
  tlsa-next <certname...>                - Output TLSA RRs for the next key
  tlsa-check [<certname...>]             - Check that TLSA RRs are published
  add-reload <certname> <service...>     - Add services to reload on rollover
  del-reload <certname> <service...>     - Delete services to reload on rollover
  show-reload <certname>                 - Show services to reload on rollover
  reload <certname...>                   - Reload services affected by rollover
  rollover <certname...>                 - Perform a key rollover
  status                                 - Show current/next status

  sshfp <domain>                         - Output SSHFP RRs for localhost
  sshfp-check <domain>                   - Check that SSHFP RRs are published
  openpgpkey <emailaddress>              - Output OPENPGPKEY RR for an email
  openpgpkey-check <emailaddress>        - Check OPENPGPKEY RR is published
  smimea <smimecert.pem>                 - Output SMIMEA RR for a certificate
  smimea-check <smimecert.pem>           - Check SMIMEA RR is published

INTRODUCTION

Danectl is a DNSSEC DANE implementation manager. It uses certbot(1) to create
and manage pairs of keys for use with a TLSA 3 1 1 current + next workflow.
It generates TLSA records for your TLS services for you to publish in the
DNS, checks that they are correctly published, and performs key rollovers.

Danectl can also generate and check SSHFP records for the local SSH server.
Danectl can also generate and check an OPENPGPKEY record for a GnuPG key.
Danectl can also generate and check an SMIMEA record for an S/MIME certificate.

DESCRIPTION

Danectl lets you create a pair of certbot certificate lineages to be used
with DANE-aware TLS clients. They are referred to as the "original" and the
"duplicate", or as the "current" and the "next". The current and next will
repeatedly swap places between the original and the duplicate certificate
lineages as the key rolls over from one to the other (with a new "next" key
being created after each rollover).

If you already have a certbot certificate lineage that you want to use with
DANE, then instead of creating both certificate lineages, you can adopt the
existing one for DANE use, and then just create the duplicate.

After that, certbot automatically renews both certificates every few months,
but the underlying keys won't change, and the TLSA records (see below) can
remain stable.

You then configure danectl with the set of port/protocol/host combinations
that you need TLSA records for. Danectl can then output the TLSA records, in
zonefile format, and you need to publish them in the DNS (somehow). Danectl
can then check that the TLSA records have been published in the DNS.

You also need to configure danectl with the list of TLS services that need
to be reloaded when the key rolls over. This is needed even when certbot is
configured to do it with deploy hooks, because those hooks are only run when
a certificate is renewed. Service reloads also need to happen when there's a
DANE key rollover, and that doesn't necessarily happen at the same time as
automatic certbot certificate renewals.

You then need to configure your TLS services to use the "current"
certificate in /etc/letsencrypt/current/<cert-name>, and then reload them.
This is like following instructions for using a certbot certificate, but
replacing "/etc/letsencrypt/live" with "/etc/letsencrypt/current".

Periodically, you can perform key rollovers on a schedule that suits you
(e.g., annually). An emergency key rollover is exactly the same.

At any time, you can show the status (which certificate lineages are
"current", which are "next", which new TLSA records are not yet published in
the DNS, and which old TLSA records have not yet been removed from the DNS).

In addition to TLSA records, you can also generate SSHFP, OPENPGPKEY, and
SMIMEA records, and check that they are published in the DNS.

OPTIONS

-h, --help

This outputs danectl's usage message, then exits.

-V, --version

This outputs danectl's name and version, then exits.

-q, --quiet

This enables quiet mode, causing danectl to pass -q to certbot. This only
affects the "new", "dup", and "rollover" commands, and is probably a good
idea, as it makes the output tidier (especially for cronjobs).

-v, --verbose

This enables verbose mode, causing danectl to print actions before
performing them.

-n, --test

This enables test mode, preventing danectl from performing any changes. It
implies verbose mode, causing danectl to print the actions that would have
been performed.

-g, --group groupname

This specifies a name for a subgroup of certificate lineages that are to be
treated in the same way as each other.

By default, danectl assumes that all certificate lineages have the same key
type and the same authentication and installation details when it invokes
certbot. In other words, the certbot command line options that are specified
and saved with danectl's "certbot" command apply to all certificate
lineages.

If your requirements are more heterogeneous, with either a different type of
key needed for different certificate lineages (for different domains or for
the same domains), or if different certificate lineages require different
authentication or installation methods, use the --group option to specify
groups of certificate lineages that are homogeneous within the group.

Each group is identified by the groupname option argument. Note that every
certificate lineage within a group must have the same key type,
authentication method, and installation method.

If you need the --group option, it must be used for all danectl commands
pertaining to the certificate lineages within a group. The groupname option
argument should only contain characters that are suitable for use in a
filename. The default configuration file for danectl is ~/.danectlrc. When
the --group option is used, a separate configuration file is used. Its name
will look like ~/.danectlrc.groupname.

Even when the --group option is used, it is still possible to use danectl
without the --group option and use the default configuration file for some
certificate lineages. This can be viewed as an unnamed default group.

Note: If you need multiple key types for the same domain(s), you will need
to create the second certificate lineage directly with certbot using its
--duplicate command line option, and then use danectl's "adopt" command. If
you try to use danectl's "new" command instead, certbot will refuse to
create the new certificate lineage for domains that already have a
certificate lineage.

-1, --oneline

This causes each long DNS record to be output on a single very long line,
rather than on multiple lines enclosed by parentheses ("(" and ")"). This
only applies to OPENPGPKEY and SMIMEA records. Each TLSA and SSHFP record is
always output on a single line.

-s, --spaces

This implies the --oneline option, and causes space characters (" ") to be
included in the output of long, single line DNS records (one every 56
characters). This only applies to OPENPGPKEY and SMIMEA records. There's
probably no real need for this.

COMMANDS

Danectl can show you this manual page:

  danectl help

You can also show the aliases for all of the commands:

  danectl aliases

Before you can use danectl to do anything interesting for TLS, you might
need to supply any command line command and/or options that certbot will
need when it creates new certificate lineages. This is for authentication
and installation. Don't use quotes and don't put spaces inside arguments
(e.g., webroot paths must not contain spaces).

  danectl certbot --apache

The default is "--apache" to use the apache plugin, if /etc/apache2 exists.
Otherwise, the default is "--nginx" to use the nginx plugin, if /etc/nginx
exists. Otherwise, the default is "--standalone" to use the standalone
plugin.

When the apache or nginx plugin is used, the default certbot command used is
"run", which installs the certificate lineage to the web server
configuration after authentication. Otherwise, the default certbot command
used is "certonly", which only authenticates, and doesn't install the
certificate lineage to any web webserver configuration. This only applies to
danectl's "new" command (see below). Danectl's "dup" command (see below)
always uses certbot's "certonly" command.

Certbot's "run" command must only be used when the new certificate is to be
installed. Otherwise, certbot's "certonly" command must be used instead. If
danectl's default certbot command is wrong for your needs, you can override
it by putting "run" or "certonly" before any certbot command line options.

This might be useful if you have apache or nginx, but don't want the
certificate installed automatically, or if you use a third-party plugin that
is capable of installing the certificate, but danectl doesn't know that.
Danectl naively assumes that all third-party plugins do not install, but
that isn't always true.

  danectl certbot certonly --nginx
  danectl certbot run --third-party-plugin-details

Danectl's "certbot" command modifies your ~/.danectlrc (or ~/.danectlrc.*)
file.

If you already have a certbot certificate lineage, you can adopt it for DANE
use:

  danectl adopt example.org

Note that you must use an existing certificate's cert-name, not the list of
certified domains. To see all of your cert-names, run "certbot certificates",
or look in /etc/letsencrypt/live.

This will create a symlink in /etc/letsencrypt/current to the adopted
certificate lineage.

If you want to create a new certbot certificate lineage for DANE instead,
use:

  danectl new example.org www.example.org mail.example.org

Note that you must provide the complete list of domain names to certify. The
cert-name of the new certificate lineage will be the first domain in the list.
It should be a base domain name.

This will create a symlink in /etc/letsencrypt/current to the new
certificate lineage.

Either way, you then need to create an additional duplicate certificate
lineage for the same set of domains:

  danectl dup example.org www.example.org mail.example.org

Note that you must provide the complete list of domain names to certify.
It must be identical to the list of domains in the adopted or new original
certificate lineage. The cert-name will be the first domain in the list
followed by "-duplicate". You won't need to use that suffix with danectl,
but you will need to use it when using certbot directly.

This will create a symlink in /etc/letsencrypt/next to the duplicate
certificate lineage.

Once a pair of certificate lineages is set up, certbot will start renewing
them automatically, but the underlying keys won't change. That means that
the TLSA records (see below) can remain stable for longer than just a few
months.

Most of the remaining commands below require the base cert-name as their
first argument. Not the list of domain names, just the first domain, or
cert-name, without any "-duplicate" suffix.

You then need to specify all of the port/protocol/host combinations that you
will want TLSA records for:

  danectl add-tlsa example.org _443._tcp _443._tcp.www
  danectl add-tlsa example.org _25._tcp.mail
  danectl add-tlsa example.org _465._tcp.mail _587._tcp.mail
  danectl add-tlsa example.org _110._tcp.mail _143._tcp.mail
  danectl add-tlsa example.org _993._tcp.mail _995._tcp.mail

Do not include the base cert-name in the port/protocol/host combinations.
It will be appended automatically when TLSA records are output.

But if the certificate lineage certifies multiple base domains, and you need
to specify a host that is not in the domain indicated by the base cert-name,
then specify the complete hostname terminated by a dot ("."). The trailing
dot prevents the base cert-name from being automatically appended.

However, it's best to use a separate certificate lineage for each base
domain. Otherwise, when danectl outputs TLSA records for a key, you will
probably (depending on how you publish DNS records) need to separate the
output for different zonefiles based on their base domains.

You can also remove port/protocol/host combinations:

  danectl del-tlsa example.org _110._tcp.mail _143._tcp.mail

The "add-tlsa" and "del-tlsa" commands modify your ~/.danectlrc
(or ~/.danectlrc.groupname) file.

You can also show which port/protocol/host combinations will be included
for TLSA records:

  danectl show-tlsa example.org

To output all TLSA records for the current key:

  danectl tlsa-current example.org

To output all TLSA records for the next key:

  danectl tlsa-next example.org

Initially, you need to publish the TLSA records for both the current and
next keys in the DNS (somehow).

To check that all TLSA records for the current and next keys are correctly
published in the DNS:

  danectl tlsa-check example.org

If no cert-name is supplied, then all cert-names are checked, and the
--reuse-key status of each certificate lineage is checked, and restored if
it is unset for any reason.

All TLSA records for the current and next keys must be published in the DNS
before you configure your services to use the current key.

To specify the services that need to be reloaded when a key rolls over:

  danectl add-reload example.org apache2 postfix dovecot

A service name can be: an absolute path to an executable file; or anything
recognized as a service name by systemctl(1), service(8), or rcctl(8)
(depending on the local operating system); or an executable file in
/etc/init.d, /etc/rc.d, or /usr/local/etc/rc.d. When services are reloaded,
the \$CERTNAME environment variable will contain the relevant certificate
name. When the service is an absolute path, the executable is invoked with
"reload" as its first command line argument (\$1), and with the certificate
name as its second argument (\$2). Also, the absolute path must not contain
any whitespace characters.

They can also be removed:

  danectl del-reload example.org postfix # Postfix looks after itself

The "add-reload" and "del-reload" commands modify your ~/.danectlrc (or
~/.danectlrc.groupname) file.

You can also show which services will be reloaded when a key rolls over:

  danectl show-reload example.org

At any point, you can reload the services that need to be reloaded when
the key rolls over:

  danectl reload example.org

But this shouldn't be necessary. It's automatic when a key rolls over.
Although it might be useful in certbot renewal hooks.

Occasionally (e.g., annually), perform a key rollover:

  danectl rollover example.org

This will redesignate the "next" key as the "current" key (and vice versa),
reload affected services, and create a new key/certificate as the new next
key. It also outputs the old TLSA records for the old current key that you
need to remove from the DNS (somehow). And it outputs the new TLSA records
for the new next key that you need to publish in the DNS (somehow).

At any time, you can show the status of all certificate pairs:

  danectl status

This will show, for each base cert-name, which certificate lineage is
"current" (original or duplicate), and which is "next". It will also show
any new TLSA records that should be, but are not, published in the DNS. It
will also show any old TLSA records that are published in the DNS, but
should no longer be.

If any of the symlinks in /etc/letsencrypt/{current,next} target certificate
lineages that no longer exist in /etc/letsencrypt/live, this is also
mentioned, and they are deleted. This indicates that the certificate lineage
was previously deleted with certbot.

If any of the certificate lineages no longer have their --reuse-key status
set for any reason, this is also mentioned, and it is restored.

You can also output SSHFP records for the local SSH server:

  danectl sshfp example.org

If you want to authorize your local SSH server's host keys to clients via
DNSSEC, you will need to publish these SSHFP records in the DNS (somehow).
You will also need to set VerifyHostKeyDNS to "yes" or "ask" in the ssh(1)
client configuration (ssh_config(5)).

To check that SSHFP records for the local SSH server are published in the
DNS:

  danectl sshfp-check example.org

You can also output an OPENPGPKEY record for the key that is associated with
an email address in your GnuPG public keyring:

  danectl openpgpkey user@example.org

The email address must be a valid argument for gpg(1)'s --export option.
If you want your correspondents to find the key, you will need to publish
the OPENPGPKEY record in the DNS (somehow), and they will need to add "dane"
to their auto-key-locate list.

To check that the OPENPGPKEY record for the email address is published in
the DNS:

  danectl openpgpkey-check user@example.org

You can also output an SMIMEA record for an S/MIME certificate:

  danectl smimea smimecert.pem

The S/MIME certificate file must be in PEM format. If you want your
correspondents to find the certificate, you will need to publish the SMIMEA
record in the DNS (somehow), and they will probably need to consult their
mail client documentation.

To check that the SMIMEA record for an S/MIME certificate is published in
the DNS:

  danectl smimea-check smimecert.pem

FILES

  /etc/letsencrypt/live    - Certbot certificate lineages
  /etc/letsencrypt/current - Current keys/certificates
  /etc/letsencrypt/next    - Next keys/certificates
  ~/.danectlrc             - Default configuration file
  ~/.danectlrc.*           - Group-specific configuration file(s)

The ~/.danectlrc configuration file is technically a shell script.
But danectl's additions and removals are purely a single name="value"
line at a time. Bear that in mind if you modify it manually. It must
be owned by the user, and it must not be group- or world-writable.

CAVEAT

If you need to add or remove any domains from a pair of certificate
lineages, you will need to do it with certbot, and you will need to do it
for both the original and the duplicate certificate lineages (e.g.,
example.org and example.org-duplicate), and you will need to use certbot's
--cert-name command line option. Since there will be two certificate
lineages with the same set of domains, it would be ambiguous otherwise.
There may be other things that you will need to use certbot directly for.
Keep things in sync.

BUGS

Danectl relies entirely on certbot. This isn't a bug. It's a choice. This
choice was made so as to make danectl easy to implement, and because it's
pragmatic. The same key can be used with DANE-aware or PKIX-aware clients.
You don't need to know which. And you don't need to exclude any clients.

But it means that it only works with keys that are created by certbot, and
with certificates that are issued by a Certificate Authority (CA). If you
want to use DANE so as to avoid the CA ecosystem entirely, danectl won't
help you do that.

On the other hand, only a single CA is involved, LetsEncrypt, not the entire
ecosystem, and danectl only generates TLSA 3 1 1 records, which only refer
to the local public key itself. There is no reliance on any keys belonging
to the CA. That's good because those keys can and will change on a schedule
that is beyond your control. But your own keys are under your control.

So danectl is opinionated and inflexible. Other things can be done with TLSA
records, but they can't be done with danectl. But what can be done with
danectl can be done easily. If you need greater flexibility, you'll need to
find it elsewhere.

For OPENPGPKEY usage, if your public keyring contains multiple keys with the
same email address keyid, danectl will only output the first one that gpg
exports. Let me know if this is a problem.

For SMIMEA usage, only SMIMEA 3 0 0 records are supported. This isn't really
a bug, as they seem to be the most/only useful ones.

Danectl doesn't actually publish or remove any DNS records for you. This
isn't really a bug. It's a limitation. There are too many ways to publish
records in the DNS. But danectl does try to make it easy for you to handle
that yourself by outputting records in the well-known BIND9 zonefile format.
When performing a key rollover, and when performing any of the checks, any
superfluous records that are to be removed are output in the form of
comments where the leading semicolon (";") is not followed by a space (" ").
That makes them easy to identify with grep (i.e., grep '^;[^ ]' for
superfluous records to be removed, and grep -v '^;[^ ]' for missing records
to be published). In either case, the grep output includes a comment that
can be removed by piping through grep -v '; ' as well. Perhaps a plugin
system could be added to automate updates for different DNS server software
and different DNS service providers, but it should be easy enough to write a
separate script to read the output of danectl and do what you need with it.
Danectl comes with two such scripts: danectl-zonefile(1), which reads the
output from danectl, then backs up and modifies the BIND9 zonefile given on
its command line, replacing old records with new ones; and
danectl-nsupdate(1), which reads the output from danectl and transforms it
into input for the nsupdate(1) dynamic DNS update utility.

REQUIREMENTS

Danectl is written in Bourne shell, and should work on any platform
that has the following prerequisites.

In all cases, danectl requires /bin/sh and host (or drill).

On systems like Solaris, /usr/xpg4/bin/sh is used instead of /bin/sh.

For TLSA usage, danectl also requires ls, sed, grep, readlink, certbot,
openssl, sha256sum, and root privileges (for certbot).

For SSHFP usage, danectl also requires sed, perl and ssh-keygen.

For OPENPGPKEY usage, danectl also requires perl and gpg.

For SMIMEA usage, danectl also requires perl and openssl.

For non-ASCII domain names, danectl also requires GNU idn2.

The danectl-zonefile output adapter requires perl.

The danectl-nsupdate output adapter requires perl.

For reloading affected services on key rollover, any system with
systemctl, service, rcctl, or service scripts in
/etc/init.d, /etc/rc.d, or /usr/local/etc/rc.d should work.

LICENSE

Danectl is released under the terms of the GPLv2+
https://www.gnu.org/licenses.

SEE ALSO

certbot(1), ssh(1), ssh_config(5), gpg(1), openssl(1), danectl-zonefile(1),
named(8), danectl-nsupdate(1), nsupdate(1), RFC6698, RFC7671, RFC7672,
RFC4255, RFC7929, RFC8162.

AUTHOR

$date $author

URL

  $url
  $git
  $git2

HELP
}

die() { echo "$name: $@" >&2; exit 1; }
warn() { echo "$name: $@" >&2; }

# Record certbot auth/install details

certbot_setup()
{
	command=""
	case "$1" in *\ *) die "certbot command argument contains space (remove quotes)";; esac
	case "$1" in run|certonly) command="$1"; shift;; esac
	options="$*"
	set_rc certbot_command "$command"
	set_rc certbot_options "$options"
}

# Adopt existing cert(s) as current

adopt()
{
	certname="$*"
	fname="$le/renewal/$certname.conf"
	[ -f "$fname" ] || die "adopt $certname: Failed to find renewal config file $fname"
	[ -e "$le/current/$certname" -o -e "$le/next/$certname" ] && die "adopt $certname: Already adopted"
	grep -q reuse_key "$fname" && del_line "$fname" reuse_key
	add_line "$fname" "reuse_key = True"
	run ln -s "$le/live/$certname" "$le/current/$certname"
}

# Default certbot authentication/installation plugin

default_certbot_plugin()
{
	if [ -d /etc/apache2 ]
	then
		echo --apache
	elif [ -d /etc/nginx ]
	then
		echo --nginx
	else
		echo --standalone
	fi
}

# Default certbot command

default_certbot_command()
{
	case "${certbot_options:-`default_certbot_plugin`}" in
		*--apache*|*--nginx*)
			echo run
			;;
		*)
			echo certonly
			;;
	esac
}

# Fetch DNS RRs using either host or drill

fetch_dns()
{
	dns_type="$1"
	dns_name="`idna $2`"

	if [ $HAVE_HOST = 1 ]
	then
		host -t "$dns_type" "$dns_name"
	else
		drill "$dns_name" "$dns_type" | grep -v '^;' | grep -v '^$' | sed 's/[0-9][0-9]*	IN	//'
	fi
}

# Create a new original/current cert

new()
{
	certname="$1"
	domains="$*"
	certname="`echo $certname | sed 's/ .*$//'`"
	domains="`echo $domains | sed 's/ /,/g'`"
	[ -d "$le/live/$certname" ] && die "new $certname: Already exists"
	run certbot "${certbot_command:-`default_certbot_command`}"$quiet ${certbot_options:-`default_certbot_plugin`} --reuse-key -d "$domains"
	[ $test = 0 -a $? != 0 ] && die "new $certname: certbot failed"
	[ $test = 0 -a ! -d "$le/live/$certname" ] && die "new $certname: certbot failed"
	run ln -s "$le/live/$certname" "$le/current/$certname"
}

# Create a new duplicate/next cert

dup()
{
	certname="$1"
	domains="$*"
	certname="`echo $certname | sed 's/ .*$//'`"
	domains="`echo $domains | sed 's/ /,/g'`"
	[ ! -d "$le/live/$certname" ] && die "dup $certname: Failed to find live original $certname"
	[ ! -d "$le/current/$certname" ] && die "dup $certname: Failed to find current original $certname"
	[ -d "$le/live/$certname-duplicate" ] && die "dup $certname: Duplicate $certname-duplicate already exists"
	run certbot certonly$quiet ${certbot_options:-`default_certbot_plugin`} --duplicate --reuse-key --cert-name "$certname-duplicate" -d "$domains"
	[ $test = 0 -a $? != 0 ] && die "dup $certname: certbot failed"
	[ $test = 0 -a ! -d "$le/live/$certname-duplicate" ] && die "dup $certname: certbot failed"
	run ln -s "$le/live/$certname-duplicate" "$le/next/$certname"
}

# Add port/pro/host for TLSA output

add_tlsa()
{
	certname="$1"; shift
	shcertname="`shcertname $certname`"
	eval tlsa="\$tlsa_$shcertname"
	set_rc tlsa_"$shcertname" "$tlsa $@"
}

# Delete port/pro/host for TLSA output

del_tlsa()
{
	certname="$1"; shift
	shcertname="`shcertname $certname`"
	eval tlsa="\$tlsa_$shcertname"
	for t in "$@"
	do
		tlsa="`echo $tlsa | remove_item \"$t\"`"
	done
	set_rc tlsa_"$shcertname" "$tlsa"
}

# Show port/pro/host for TLSA output

show_tlsa()
{
	certname="$1"; shift
	shcertname="`shcertname $certname`"
	eval tlsa="\$tlsa_$shcertname"
	echo "$tlsa"
}

# Output TLSA RRs for the current/next key

tlsa_role()
{
	role="$1"
	certname="$2"
	label="$3"
	prefix="$4"
	shcertname="`shcertname $certname`"
	eval tlsa="\$tlsa_$shcertname"
	[ -z "$tlsa" ] && die "No TLSA records have been configured yet for $certname"
	hash="`tlsa_hash $role $certname`"
	echo "$prefix; $certname $label"
	for t in $tlsa
	do
		domain="`case \"$t\" in *.) echo $t;; *) echo $t.$certname.;; esac`"
		printf "$prefix`idna $domain`\tIN\tTLSA\t3 1 1 $hash\n"
	done
}

# Output TLSA RRs for the current key

tlsa_current()
{
	certname="$1"
	label="${2:-current}"
	tlsa_role current "$certname" "$label"
}

# Output TLSA RRs for the next key

tlsa_next()
{
	certname="$1"
	label="${2:-next}"
	prefix="$3"
	tlsa_role next "$certname" "$label" "$prefix"
}

# Check that TLSA RRs are published

tlsa_check()
{
	certname="$1"
	shcertname="`shcertname $certname`"
	eval tlsa="\$tlsa_$shcertname"
	[ -z "$tlsa" ] && die "No TLSA records have been configured yet for $certname"
	hash_current="`tlsa_hash current $certname`"
	hash_next="`tlsa_hash next $certname`"
	missing=0
	for t in $tlsa
	do
		domain="`case \"$t\" in *.) echo $t;; *) echo $t.$certname.;; esac`"
		if fetch_dns tlsa "$domain" | sed 's/ \([a-fA-F0-9][a-fA-F0-9]*\)$/\1/' | grep -iq "$hash_current"
		then
			:
		else
			[ "$missing" = 0 ] && echo "; Missing $certname current (must be published)"
			missing=1
			printf "`idna $domain`\tIN\tTLSA\t3 1 1 $hash_current\n"
		fi
	done
	missing=0
	for t in $tlsa
	do
		domain="`case \"$t\" in *.) echo $t;; *) echo $t.$certname.;; esac`"
		if fetch_dns tlsa "$domain" | sed 's/ \([a-fA-F0-9][a-fA-F0-9]*\)$/\1/' | grep -iq "$hash_next"
		then
			:
		else
			[ "$missing" = 0 ] && echo "; Missing $certname next (must be published)"
			missing=1
			printf "`idna $domain`\tIN\tTLSA\t3 1 1 $hash_next\n"
		fi
	done
	superfluous=0
	for t in $tlsa
	do
		domain="`case \"$t\" in *.) echo $t;; *) echo $t.$certname.;; esac`"
		extra="`fetch_dns tlsa \"$domain\" | grep TLSA | sed -e 's/^/;/' -e 's/ has /.	/' -e 's/ record /	/' -e 'y/ABCDEF/abcdef/' -e 's/TLSa/TLSA/' -e 's/ \([a-f0-9][a-f0-9]*\)$/\1/' | grep -v \"$hash_current\" | grep -v \"$hash_next\"`"
		if [ -n "$extra" ]
		then
			[ "$superfluous" = 0 ] && echo ";; Superfluous $certname (should be removed)"
			superfluous=1
			echo "$extra"
		fi
	done
}

# Add services to reload on rollover

add_reload()
{
	certname="$1"; shift
	shcertname="`shcertname $certname`"
	eval reload="\$reload_$shcertname"
	set_rc reload_"$shcertname" "$reload $@"
}

# Delete services to reload on rollover

del_reload()
{
	certname="$1"; shift
	shcertname="`shcertname $certname`"
	eval reload="\$reload_$shcertname"
	for t in "$@"
	do
		reload="`echo $reload | remove_item \"$t\"`"
	done
	set_rc reload_"$shcertname" "$reload"
}

# Show services to reload on rollover

show_reload()
{
	certname="$1"; shift
	shcertname="`shcertname $certname`"
	eval reload="\$reload_$shcertname"
	echo "$reload"
}

# Reload services affected by rollover

reload()
{
	certname="$1"
	CERTNAME="$certname"; export CERTNAME
	shcertname="`shcertname $certname`"
	eval reload="\$reload_$shcertname"
	for service in $reload
	do
		case "$service" in /*) if [ -x "$service" ]; then run $service reload "$CERTNAME"; continue; fi;; esac
		case "`which systemctl 2>/dev/null`" in /*) run systemctl reload "$service"; continue;; esac
		case "`which service 2>/dev/null`" in /*) run service "$service" reload; continue;; esac
		case "`which rcctl 2>/dev/null`" in /*) run rcctl reload "$service"; continue;; esac
		if [ -x /etc/init.d/"$service" ]; then run /etc/init.d/"$service" reload; continue; fi
		if [ -x /etc/rc.d/"$service" ]; then run /etc/rc.d/"$service" reload; continue; fi
		if [ -x /usr/local/etc/rc.d/"$service" ]; then run /usr/local/etc/rc.d/"$service" reload; continue; fi
		warn "Unable to reload $service"
	done
}

# Perform a key rollover

rollover()
{
	certname="$1"
	[ ! -d "$le/current/$certname" ] && die "rollover $certname: Failed to find current $certname"
	[ ! -d "$le/next/$certname" ] && die "rollover $certname: Failed to find next $certname"
	# Identify the transition: original to duplicate, or duplicate to original
	if readlink "$le/current/$certname" | grep -q ".-duplicate"
	then
		curr="-duplicate"
		next=""
	else
		curr=""
		next="-duplicate"
	fi
	# Perform the key rollover
	run rm "$le/next/$certname"
	run rm "$le/current/$certname"
	run ln -s "$le/live/$certname$next" "$le/current/$certname"
	run ln -s "$le/live/$certname$curr" "$le/next/$certname"
	reload "$certname"
	# Swap $curr and $next
	if [ "x$curr" = "x" ]
	then
		curr="-duplicate"
		next=""
	else
		curr=""
		next="-duplicate"
	fi
	# Show the old TLSA records that need to be removed from the DNS
	echo ";; Remove the following old records from the DNS"
	tlsa_next "$certname" previous ";"
	# Create the new next key/certificate
	del_line "$le/renewal/$certname$next.conf" reuse_key
	run certbot renew$quiet --force-renewal --cert-name "$certname$next"
	rc=$?
	add_line "$le/renewal/$certname$next.conf" "reuse_key = True"
	[ $test = 0 -a $rc != 0 ] && die "rollover $certname: certbot failed after the rollover! You must successfully run: certbot renew --force-renewal --cert-name \"$certname$next\" (but first remove \"reuse_key = True\" from $le/renewal/$certname$next.conf and then put it back afterwards)"
	# Show the new TLSA records that need to be published in the DNS
	echo "; Publish the following new records in the DNS"
	tlsa_next "$certname"
}

# Show current/next status

status()
{
	for role in current next
	do
		for symlink in "$le/$role"/*
		do
			[ -h "$symlink" ] || continue
			certname="${symlink##*/}"
			target="`readlink \"$symlink\"`"
			note=; [ ! -d "$symlink" ] && note=" (deleted)" && run rm "$symlink"
			echo "$certname: $role ${target##*/}$note"
		done
	done | sort
	for symlink in "$le/current"/*
	do
		[ -h "$symlink" ] || continue
		tlsa_check "${symlink##*/}"
	done
}

# Check/ensure that all certificate renewals will reuse the key

reuse_key_check()
{
	prefix="$1"
	for role in current next
	do
		for symlink in "$le/$role"/*
		do
			[ -h "$symlink" ] || continue
			[ -d "$symlink" ] || continue
			target="`readlink \"$symlink\"`"
			certname="${target##*/}"
			fname="$le/renewal/$certname.conf"
			[ -f "$fname" ] || continue
			grep -q "reuse_key = True" "$fname" && continue
			del_line "$fname" reuse_key
			add_line "$fname" "reuse_key = True"
			echo "$prefix$certname: Not reusing key on renewal (fixed)"
		done
	done | sort
}

# Output the public key hash for the given role and cert-name

tlsa_hash()
{
	role="$1"
	certname="$2"
	cert="$le/$role/$certname/cert.pem"
	openssl x509 -in "$cert" -noout -pubkey | openssl pkey -pubin -outform DER 2>/dev/null | sha256sum | sed 's/\s.*$//'
}

# Run a command unless testing, print it first if verbose

run()
{
	[ $verbose = 1 ] && echo "$@"
	[ $test = 0 ] && "$@"
}

# Add a line to a file unless testing, print it first if verbose

add_line()
{
	fname="$1"; shift
	line="$*"
	line="`echo $line | sed -e 's/\s+/ /g' -e 's/^ //' -e 's/ $//'`"
	[ $verbose = 1 ] && echo "Add \"$line\" to $fname"
	[ $test = 0 ] && echo "$line" >> "$fname"
}

# Delete a line from a file unless testing, print it first if verbose

del_line()
{
	fname="$1"; shift
	line="$*"
	[ $verbose = 1 ] && echo "Remove \"$line\" from $fname"
	[ $test = 1 ] && return
	grep -v "^$line" "$fname" > "$fname.tmp.$$"
	mv "$fname.tmp.$$" "$fname"
}

# Add a line to ~/.danectlrc

add_rc()
{
	[ -f "$danectlrc" ] || touch "$danectlrc"
	add_line "$danectlrc" "$@"
}

# Delete a line from ~/.danectlrc

del_rc()
{
	[ -f "$danectlrc" ] || touch "$danectlrc"
	del_line "$danectlrc" "$@"
}

# Set or replace a variable in ~/.danectlrc

set_rc()
{
	varname="$1"; shift
	value="$*"
	value="`echo \"$value\" | sed -e 's/^ //'`"
	del_rc "$varname="
	add_rc "$varname=\"$value\""
}

# Remove something from a space-separated list (stdin/stdout)

remove_item()
{
	item="$1"
	sed 's/ /\n/g' | grep -v "$item"
}

# Output the ASCII version (IDNA2008/TR46) of a Unicode domain name 

idna()
{
	domainname="$1"
	if echo "$domainname" | LANG=C grep -q '^[a-zA-Z0-9-][a-zA-Z0-9.-]*$'
	then
		echo $domainname
	else
		case "`which idn2 2>/dev/null`" in /*) ;; *) die "Failed to find idn2";; esac
		idn2 -l -- "$domainname"
	fi
}

# Output a form of certname that is suitable for use in a shell variable identifier

shcertname()
{
	certname="$1"
	idna "$certname" | LANG=C sed 's/[^a-zA-Z0-9]/_/g'
}

# Check for host or drill

check_host_or_drill()
{
	case "`which host 2>/dev/null`" in
		/*) [ -z "$DANECTL_TEST_DRILL" ] && HAVE_HOST=1 || HAVE_HOST=0;;
		*)  HAVE_HOST=0;;
	esac

	case "`which drill 2>/dev/null`" in
		/*) [ -z "$DANECTL_TEST_HOST" ] && HAVE_DRILL=1 || HAVE_DRILL=0;;
		*)  HAVE_DRILL=0;;
	esac

	[ $HAVE_HOST = 0 -a $HAVE_DRILL = 0 ] && die "Failed to find host or drill"

	export HAVE_HOST # For any perl sub-processes
}

# Check the prerequisites

check_prerequisites()
{
	[ -d "$le" ] || die "Failed to find $le (install certbot)"
	[ -w "$le" ] || die "Unable to write to $le (use sudo)"
	[ ! -d "$le/current" ] && mkdir "$le/current"
	[ ! -d "$le/next" ] && mkdir "$le/next"

	for cmd in ls sed grep readlink certbot openssl sha256sum
	do
		case "`which $cmd 2>/dev/null`" in /*) ;; *) die "Failed to find $cmd";; esac
	done

	check_host_or_drill

	if [ -e "$danectlrc" ]
	then
		[ ! -O "$danectlrc" ] && die "$danectlrc is not owned by root"
		case "`ls -lL \"$danectlrc\"`" in
			-rw-r--r--*|-rw-r-----*|-rw-------*)
				;;
			*)
				die "$danectlrc is group- or world-writable"
				;;
		esac
		. "$danectlrc"
	fi
}

# Check the cert-names

check_certnames()
{
	for certname in "$@"
	do
		[ -d "$le/live/$certname" ] || die "Failed to find certname $certname"
	done
}

# Check the base cert-name and the duplicate version

check_both_certnames()
{
	for certname in "$@"
	do
		[ -d "$le/live/$certname" ] || die "Failed to find certname $certname"
		[ -d "$le/live/$certname-duplicate" ] || die "Failed to find certname $certname-duplicate"
	done
}

# Output all DANE certnames

all_certnames()
{
	for symlink in "$le"/current/*
	do
		[ -h "$symlink" ] || continue
		certname="${symlink##*/}"
		target="`readlink \"$symlink\"`"
		[ ! -d "$symlink" ] && continue
		[ ! -d "$le/next/$certname" ] && continue
		echo "$certname"
	done
}

# Check the prerequisites for SSHFP

check_sshfp_prerequisites()
{
	for cmd in sed perl ssh-keygen
	do
		case "`which $cmd 2>/dev/null`" in /*) ;; *) die "Failed to find $cmd";; esac
	done

	check_host_or_drill
}

# Output SSHFP RRs for the current host

sshfp()
{
	hostname="`idna $1`"
	ssh-keygen -r "$hostname" | sed -e 's/ /.	/' -e 's/ /	/' -e 's/ /	/'
}

# Check that SSHFP RRs are published

sshfp_check()
{
	hostname="`idna $1`"
	perl -e '
		use strict;
		use warnings;
		my %current = map { $_ =~ /SSHFP (\d+ \d+ \S+)$/; ($1, 1) } split /\n/, `ssh-keygen -r "'"$hostname"'"`;
		my $fetch_dns_sshfp = ($ENV{HAVE_HOST} eq "1") ? `host -t sshfp "'"$hostname"'" | grep -v "has no .* record"` : `drill "'"$hostname"'" sshfp | grep -v "^;" | grep -v "^\$"`;
		my %published = map { $_ =~ /SSHFP(?: record)?\s+(\d+ \d+ \S+)(?:\s+(\S+))?$/; (lc($1 . ($2 // "")), 1) } grep { /SSHFP/ } split /\n/, $fetch_dns_sshfp;
		my $missing = 0;
		for my $sshfp (sort keys %current)
		{
			print(($missing ? "" : "; Missing '"$hostname"' sshfp (must be published)\n"), "'"$hostname"'.\tIN\tSSHFP\t$sshfp\n"), $missing = 1 if !exists $published{$sshfp};
		}
		my $superfluous = 0;
		for my $sshfp (sort keys %published)
		{
			print(($superfluous ? "" : ";; Superfluous '"$hostname"' sshfp (should be removed)\n"), ";", "'"$hostname"'.\tIN\tSSHFP\t$sshfp\n"), $superfluous = 1 if !exists $current{$sshfp};
		}
	'
}

# Check the prerequisites for OPENPGPKEY

check_openpgpkey_prerequisites()
{
	for cmd in gpg perl
	do
		case "`which $cmd 2>/dev/null`" in /*) ;; *) die "Failed to find $cmd";; esac
	done

	check_host_or_drill
}

# Output OPENPGPKEY RR for an email

openpgpkey()
{
	email="$1"
	# Convert gpg's hex output to base64
	gpg --export-options export-dane --export "$email" | perl -e '
		use strict;
		use warnings;
		sub base64
		{
			pos $_[0] = 0;
			my $padlen = (3 - length($_[0]) % 3) % 3;
			my $encoded = join "", map { pack("u", $_) =~ /^.(\S*)/ } $_[0] =~ /(.{1,45})/gs;
			$encoded =~ tr{` -_}{AA-Za-z0-9+/};
			$encoded =~ s/.{$padlen}$/"=" x $padlen/e if $padlen;
			$encoded =~ s/(.{1,56})/\t$1\n/g if '$oneline' == 0;
			$encoded =~ s/^\t// if '$oneline' == 0;
			$encoded =~ s/(.{1,56})/ $1/g if '$oneline' == 1 && '$spaces' == 1;
			$encoded =~ s/^ // if '$oneline' == 1 && '$spaces' == 1;
			return $encoded;
		}
		sub idna
		{
			my $domain = shift;
			return $domain if $domain =~ /^[a-zA-Z0-9.-]+$/;
			chop($domain = `idn2 -l -- $domain`);
			return $domain;
		}
		my $origin;
		my $prefix;
		my $comment = "";
		my $key_hex = "";
		while (<>)
		{
			$origin = idna($1), next if /^\$ORIGIN (\S+)/;
			$comment .= $_, next if /^;/;
			$prefix = $1, next if /^([0-9a-f]+) TYPE61 \\# \d+ \($/;
			$key_hex .= $1, next if /^\s+([0-9a-f]+)$/;
			last if /^\s+\)$/;
		}
		while (<>) {}
		exit unless $origin && $prefix && $key_hex;
		my $key = base64(pack("H*", $key_hex));
		my $start = ('$oneline' == 0) ? "(\n\t" : "";
		my $end = ('$oneline' == 0) ? "\t)" : "";
		print("$comment$prefix.$origin\tIN\tOPENPGPKEY\t$start$key$end\n");
	'
}

# Check OPENPGPKEY RR is published

openpgpkey_check()
{
	email="$1"
	gpg --export-options export-dane --export "$email" | perl -e '
		use strict;
		use warnings;
		use Digest;
		# Get the key to check
		sub base64
		{
			pos $_[0] = 0;
			my $padlen = (3 - length($_[0]) % 3) % 3;
			my $encoded = join "", map { pack("u", $_) =~ /^.(\S*)/ } $_[0] =~ /(.{1,45})/gs;
			$encoded =~ tr{` -_}{AA-Za-z0-9+/};
			$encoded =~ s/.{$padlen}$/"=" x $padlen/e if $padlen;
			$encoded =~ s/(.{1,56})/\t$1\n/g if '$oneline' == 0;
			$encoded =~ s/^\t// if '$oneline' == 0;
			$encoded =~ s/(.{1,56})/ $1/g if '$oneline' == 1 && '$spaces' == 1;
			$encoded =~ s/^ // if '$oneline' == 1 && '$spaces' == 1;
			return $encoded;
		}
		sub idna
		{
			my $domain = shift;
			return $domain if $domain =~ /^[a-zA-Z0-9.-]+$/;
			chop($domain = `idn2 -l -- $domain`);
			return $domain;
		}
		my $origin;
		my $prefix;
		my $comment = "";
		my $key_hex = "";
		while (<>)
		{
			$origin = idna($1), next if /^\$ORIGIN (\S+)/;
			$comment .= $_, next if /^;/;
			$prefix = $1, next if /^([0-9a-f]+) TYPE61 \\# \d+ \($/;
			$key_hex .= $1, next if /^\s+([0-9a-f]+)$/;
			last if /^\s+\)$/;
		}
		while (<>) {} # Skip the same key appearing with multiple keyids
		my $key_exists = $origin && $prefix && $key_hex;
		my $key_base64 = base64(pack("H*", $key_hex)) if $key_exists;
		my $email = q('"$email"');
		my ($localpart, $domain) = $email =~ /^([^@]+)@([^@]+)$/;
		die("'$name': Invalid email address: $email\n") unless defined $localpart && defined $domain;
		if (!$key_exists)
		{
			my $hash = Digest->new("SHA-256");
			$hash->add($localpart);
			$prefix = substr($hash->hexdigest(), 0, 28 * 2);
			$origin = "_openpgpkey." . idna($domain);
		}
		# Is it published? Are any other keys published?
		my $missing = $key_exists;
		my $superfluous = "";
		my $start = ('$oneline' == 0) ? "(\n\t" : "";
		my $end = ('$oneline' == 0) ? "\t)" : "";
		my $fetch_dns_openpgpkey = ($ENV{HAVE_HOST} eq "1") ? `host -t openpgpkey "$prefix.$origin" | grep -v "has no .* record"` : `drill -t "$prefix.$origin" openpgpkey | grep -v "^;" | grep -v "^\$" | sed "s/	[0-9][0-9]*	IN	/ /"`;
		for (grep { /OPENPGPKEY/ } split /\n/, $fetch_dns_openpgpkey)
		{
			my ($rrdata) = $_ =~ /^\S+(?: has)?\s+OPENPGPKEY(?: record)?\s+(.+)$/;
			$rrdata =~ s/ //g;
			$rrdata =~ s/(.{1,56})/ $1/g;
			$rrdata =~ s/^ //g;
			$rrdata =~ s/ /\n\t/g if '$oneline' == 0;
			$rrdata = "$rrdata\n" if '$oneline' == 0;
			$rrdata =~ s/ //g if '$oneline' == 1 && '$spaces' == 0;
			$missing = 0, next if $key_base64 && $rrdata eq $key_base64;
			$superfluous .= "$comment$prefix.$origin\tIN\tOPENPGPKEY\t$start$rrdata$end\n";
		}
		print("; Missing $email openpgpkey (should be published)\n$comment$prefix.$origin\tIN\tOPENPGPKEY\t$start$key_base64$end\n") if $missing;
		$superfluous =~ s/^/;/mg if $superfluous;
		print(";; Superfluous $email openpgpkey (should be removed)\n$superfluous") if $superfluous;
	'
}

# Check the prerequisites for SMIMEA

check_smimea_prerequisites()
{
	for cmd in openssl perl
	do
		case "`which $cmd 2>/dev/null`" in /*) ;; *) die "Failed to find $cmd";; esac
	done

	check_host_or_drill
}

# Output SMIMEA RR for a certificate

smimea()
{
	cert="$1"
	[ ! -f "$cert" ] && die "$cert is not a file"
	openssl x509 -inform pem -in "$cert" -noout -purpose | grep -q 'S/MIME encryption : Yes' || die "$cert is not an S/MIME certificate"
	email="`openssl x509 -inform pem -in \"$cert\" -noout -text | perl -e '
		use strict;
		use warnings;
		while (<>)
		{
			print("$1\n") if /emailAddress = (\S+)/;
		}
	'`"
	[ -z "$email" ] && die "Failed to find an email address in the certificate"
	openssl x509 -inform pem -in "$cert" -outform der | perl -e '
		use strict;
		use warnings;
		use Digest;
		binmode(STDIN);
		$/ = undef;
		sub idna
		{
			my $domain = shift;
			return $domain if $domain =~ /^[a-zA-Z0-9.-]+$/;
			chop($domain = `idn2 -l -- $domain`);
			return $domain;
		}
		my $cert = unpack("H*", <STDIN>);
		$cert =~ s/(.{1,56})/\t$1\n/g if '$oneline' == 0;
		$cert =~ s/^\t// if '$oneline' == 0;
		$cert =~ s/(.{1,56})/ $1/g if '$oneline' == 1 && '$spaces' == 1;
		$cert =~ s/^ // if '$oneline' == 1 && '$spaces' == 1;
		my $email = q('"$email"');
		my ($localpart, $domain) = $email =~ /^([^@]+)@([^@]+)$/;
		die("'$name': Invalid email address: $email\n") unless defined $localpart && defined $domain;
		my $hash = Digest->new("SHA-256");
		$hash->add($localpart);
		my $prefix = substr($hash->hexdigest(), 0, 28 * 2);
		my $origin = "_smimecert." . idna($domain) . ".";
		print("; $email\n");
		my $start = ('$oneline' == 0) ? "(\n\t" : "";
		my $middle = ('$oneline' == 0) ? "\n\t" : " ";
		my $end = ('$oneline' == 0) ? "\t)" : "";
		print("$prefix.$origin\tIN\tSMIMEA\t${start}3 0 0$middle$cert$end\n");
	'
}

# Check SMIMEA RR is published

smimea_check()
{
	cert="$1"
	[ ! -f "$cert" ] && die "$cert is not a file"
	openssl x509 -inform pem -in "$cert" -noout -purpose | grep -q 'S/MIME encryption : Yes' || die "$cert is not an S/MIME certificate"
	email="`openssl x509 -inform pem -in \"$cert\" -noout -text | perl -e '
		use strict;
		use warnings;
		while (<>)
		{
			print("$1\n") if /emailAddress = (\S+)/;
		}
	'`"
	[ -z "$email" ] && die "Failed to find an email address in the certificate"
	openssl x509 -inform pem -in "$cert" -outform der | perl -e '
		use strict;
		use warnings;
		use Digest;
		binmode(STDIN);
		$/ = undef;
		sub idna
		{
			my $domain = shift;
			return $domain if $domain =~ /^[a-zA-Z0-9.-]+$/;
			chop($domain = `idn2 -l -- $domain`);
			return $domain;
		}
		my $cert = unpack("H*", <STDIN>);
		$cert =~ s/(.{1,56})/\t$1\n/g if '$oneline' == 0;
		$cert =~ s/^\t// if '$oneline' == 0;
		$cert =~ s/(.{1,56})/ $1/g if '$oneline' == 1 && '$spaces' == 1;
		$cert =~ s/^ // if '$oneline' == 1 && '$spaces' == 1;
		my $email = q('"$email"');
		my ($localpart, $domain) = $email =~ /^([^@]+)@([^@]+)$/;
		die("'$name': Invalid email address: $email\n") unless defined $localpart && defined $domain;
		my $hash = Digest->new("SHA-256");
		$hash->add($localpart);
		my $comment = "; $email\n";
		my $prefix = substr($hash->hexdigest(), 0, 28 * 2);
		my $origin = "_smimecert." . idna($domain) . ".";
		my $missing = 1;
		my $superfluous = "";
		my $start = ('$oneline' == 0) ? "(\n\t" : "";
		my $middle = ('$oneline' == 0) ? "\n\t" : " ";
		my $end = ('$oneline' == 0) ? "\t)" : "";
		my $lead = ('$oneline' == 0) ? "\t" : "";
		my $fetch_dns_smimea = ($ENV{HAVE_HOST} eq "1") ? `host -t smimea "$prefix.$origin" | grep -v "has no .* record"` : `drill -t "$prefix.$origin" smimea | grep -v "^;" | grep -v "^\$" | sed "s/	[0-9][0-9]*	IN	/ /"`;
		for (grep { /SMIMEA/ } split /\n/, $fetch_dns_smimea)
		{
			my ($rrdata) = $_ =~ /^\S+(?: has)?\s+SMIMEA(?: record)?\s+(.+)$/;
			$rrdata = lc($rrdata);
			$rrdata =~ s/^3\s*0\s*0\s*//;
			$rrdata =~ s/ //g;
			$rrdata =~ s/(.{1,56})/ $1/g;
			$rrdata =~ s/^ //g;
			$rrdata =~ s/ /\n\t/g if '$oneline' == 0;
			$rrdata .= "\n" if '$oneline' == 0;
			$rrdata =~ s/ //g if '$oneline' == 1 && '$spaces' == 0;
			$missing = 0, next if $rrdata eq $cert;
			$superfluous .= "$comment$prefix.$origin\tIN\tSMIMEA\t${start}3 0 0$middle$rrdata$end\n";
		}
		print("; Missing $email smimea (should be published)\n$comment$prefix.$origin\tIN\tSMIMEA\t${start}3 0 0$middle$cert$end\n") if $missing;
		$superfluous =~ s/^/;/mg if $superfluous;
		print(";; Superfluous $email smimea (should be removed)\n$superfluous") if $superfluous;
	'
}

# Handle any command line options

if [ $# = 0 ]
then
	usage 1 'Missing command' | ${PAGER:-more}
	exit 1
fi

while :
do
	case "$1" in
		-h|--help)
			usage 0
			;;
		-V|--version)
			echo "$name-$version"
			exit 0
			;;
		-v|--verbose)
			verbose=1
			shift
			;;
		-q|--quiet)
			quiet=' -q'
			shift
			;;
		-n|--test)
			verbose=1
			test=1
			shift
			;;
		-g|--group)
			[ "$danectlrc" = "$default_danectlrc" ] || die "The --group option may only appear once on the command line"
			shift
			[ -z "$1" ] && die "The --group option is missing its groupname argument"
			case "$1" in */*) die "Invalid --group groupname argument: $1 (must be usable as a filename)";; esac
			danectlrc="$danectlrc.$1"
			shift
			;;
		-1|--oneline)
			oneline=1
			shift
			;;
		-s|--spaces)
			oneline=1
			spaces=1
			shift
			;;
		--)
			shift
			;;
		--*)
			die "Unknown option $1"
			;;
		-*)
			die "Unknown option $1 (or unsupported option bundling)"
			;;
		*)
			break
			;;
	esac
done

# Handle the commands

command="$1"
[ $# != 0 ] && shift

case "$command" in
	help|h)
		help | ${PAGER:-more}
		;;

	certbot|cb|c)
		[ $# = 0 ] && usage 1 "Missing certbot argument(s) (expected (run or certonly) and/or certbot options)"
		check_prerequisites
		certbot_setup "$@"
		;;

	adopt|ad)
		[ $# -lt 1 ] && usage 1 "Missing adopt argument (expected a cert-name)"
		[ $# -gt 1 ] && usage 1 "Too many adopt arguments (expected a cert-name)"
		check_prerequisites
		check_certnames "$1"
		adopt "$@"
		;;

	new|original|n|o)
		[ $# = 0 ] && usage 1 "Missing new argument(s) (expected one or more domains to certify)"
		check_prerequisites
		new "$@"
		;;

	dup|duplicate|d)
		[ $# = 0 ] && usage 1 "Missing dup argument(s) (expected one or more domains to certify)"
		check_prerequisites
		dup "$@"
		;;

	add-tlsa|tlsa-add|at|ta|add|tlsa)
		[ $# -lt 2 ] && usage 1 "Missing add-tlsa argument(s) (expected a cert-name and one or more port/protocol/host combinations)"
		check_prerequisites
		check_certnames "$1"
		add_tlsa "$@"
		;;

	del-tlsa|delete-tlsa|tlsa-del|tlsa-delete|dt|td|del|delete)
		[ $# -lt 2 ] && usage 1 "Missing del-tlsa argument(s) (expected a cert-name and one or more port/protocol/host combinations)"
		check_prerequisites
		check_certnames "$1"
		del_tlsa "$@"
		;;

	show-tlsa|tlsa-show|st|ts|show)
		[ $# -lt 1 ] && usage 1 "Missing show-tlsa argument (expected a cert-name)"
		[ $# -gt 1 ] && usage 1 "Too many show-tlsa arguments (expected a cert-name)"
		check_prerequisites
		check_certnames "$1"
		show_tlsa "$@"
		;;

	tlsa-current|current-tlsa|current|tc|ct|curr)
		[ $# = 0 ] && usage 1 "Missing tlsa-current argument(s) (expected one or more cert-names)"
		check_prerequisites
		check_certnames "$@"
		for certname in "$@"; do tlsa_current "$certname"; done
		;;

	tlsa-next|next-tlsa|next|tn|nt)
		[ $# = 0 ] && usage 1 "Missing tlsa-next argument(s) (expected one or more cert-names)"
		check_prerequisites
		check_certnames "$@"
		for certname in "$@"; do tlsa_next "$certname"; done
		;;

	tlsa-check|check-tlsa|check|tch|cht|ch|chk)
		check_prerequisites
		[ $# != 0 ] && check_both_certnames "$@"
		for certname in ${*:-`all_certnames`}; do tlsa_check "$certname"; done
		[ $# = 0 ] && reuse_key_check '; '
		;;

	add-reload|reload-add|ar|ra)
		[ $# -lt 2 ] && usage 1 "Missing add-reload argument(s) (expected a cert-name and one or more service names)"
		check_prerequisites
		check_certnames "$1"
		add_reload "$@"
		;;

	del-reload|delete-reload|reload-del|reload-delete|dr|rd)
		[ $# -lt 2 ] && usage 1 "Missing del-reload argument(s) (expected a cert-name and one or more service names)"
		check_prerequisites
		check_certnames "$1"
		del_reload "$@"
		;;

	show-reload|reload-show|sr|rs)
		[ $# -lt 1 ] && usage 1 "Missing show-reload argument (expected a cert-name)"
		[ $# -gt 1 ] && usage 1 "Too many show-reload arguments (expected a cert-name)"
		check_prerequisites
		check_certnames "$1"
		show_reload "$@"
		;;

	reload|r)
		[ $# = 0 ] && usage 1 "Missing reload argument(s) (expected one or more cert-names)"
		check_prerequisites
		check_certnames "$@"
		for certname in "$@"; do reload "$certname"; done
		;;

	rollover|ro)
		[ $# = 0 ] && usage 1 "Missing rollover argument(s) (expected one or more cert-names)"
		check_prerequisites
		check_both_certnames "$@"
		for certname in "$@"; do rollover "$certname"; done
		;;

	status|s)
		[ $# != 0 ] && usage 1 "Too many status arguments (expected no additional arguments)"
		check_prerequisites
		status
		reuse_key_check
		;;

	sshfp|ssh)
		[ $# -lt 1 ] && usage 1 "Missing sshfp argument (expected a domain)"
		[ $# -gt 1 ] && usage 1 "Too many sshfp arguments (expected a domain)"
		check_sshfp_prerequisites
		sshfp "$@"
		;;

	sshfp-check|check-sshfp|ssh-check|check-ssh|sch|schk)
		[ $# -lt 1 ] && usage 1 "Missing sshfp-check argument (expected a domain)"
		[ $# -gt 1 ] && usage 1 "Too many sshfp-check arguments (expected a domain)"
		check_sshfp_prerequisites
		sshfp_check "$@"
		;;

	openpgpkey|pgpkey|gpgkey|pgp|gpg|p|g)
		[ $# -lt 1 ] && usage 1 "Missing openpgpkey argument (expected an email address / gnupg keyid)"
		[ $# -gt 1 ] && usage 1 "Too many openpgpkey arguments (expected an email address / gnupg keyid)"
		check_openpgpkey_prerequisites
		openpgpkey "$@"
		;;

	openpgpkey-check|check-openpgpkey|pgpkey-check|check-pgpkey|gpgkey-check|check-gpgkey|pgp-check|check-pgp|gpg-check|check-gpg|pc|cp|gc|cg)
		[ $# -lt 1 ] && usage 1 "Missing openpgpkey-check argument (expected an email address / gnupg keyid)"
		[ $# -gt 1 ] && usage 1 "Too many openpgpkey-check argument (expected an email address / gnupg keyid)"
		check_openpgpkey_prerequisites
		openpgpkey_check "$@"
		;;

	smimea|smime|sm)
		[ $# -lt 1 ] && usage 1 "Missing smimea argument (expected an S/MIME certificate filename)"
		[ $# -gt 1 ] && usage 1 "Too many smimea arguments (expected an S/MIME certificate filename)"
		check_smimea_prerequisites
		smimea "$@"
		;;

	smimea-check|check-smimea|smime-check|check-smime|smc|csm)
		[ $# -lt 1 ] && usage 1 "Missing smimea-check argument (expected an S/MIME certificate filename)"
		[ $# -gt 1 ] && usage 1 "Too many smimea-check arguments (expected an S/MIME certificate filename)"
		check_smimea_prerequisites
		smimea_check "$@"
		;;

	aliases|alias|a):
		echo "help: h"
		echo "aliases: alias a"
		echo "certbot: cb c"
		echo "adopt: ad"
		echo "new: original n o"
		echo "dup: duplicate d"
		echo "add-tlsa: tlsa-add at ta add tlsa"
		echo "del-tlsa: delete-tlsa tlsa-del tlsa-delete dt td del delete"
		echo "show-tlsa: tlsa-show st ts show"
		echo "tlsa-current: current-tlsa current tc ct curr"
		echo "tlsa-next: next-tlsa next tn nt"
		echo "tlsa-check: check-tlsa check tch cht ch chk"
		echo "add-reload: reload-add ar ra"
		echo "del-reload: delete-reload reload-del reload-delete dr rd"
		echo "show-reload: reload-show sr rs"
		echo "reload: r"
		echo "rollover: ro"
		echo "status: s"
		echo "sshfp: ssh"
		echo "sshfp-check: check-sshfp ssh-check check-ssh sch schk"
		echo "openpgpkey: pgpkey gpgkey pgp gpg p g"
		echo "openpgpkey-check: check-openpgpkey pgpkey-check check-pgpkey gpgkey-check check-gpgkey pgp-check check-pgp gpg-check check-gpg pc cp gc cg"
		echo "smimea: smime sm"
		echo "smimea-check: check-smimea smime-check check-smime smc csm"
		;;

	'')
		usage 1 'Missing command' | ${PAGER:-more}
		exit 1
		;;

	*)
		usage 1 "Unknown command: $command" | ${PAGER:-more}
		exit 1
		;;
esac

exit $?

# vi:set ts=4 sw=4:
