From 99151a2db842a850a2860af3e77532370802ca69 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Tue, 3 Jun 2025 10:49:05 +0100 Subject: make the challenge handler configurable perhaps one day we'll even support something other than Kerberos! --- .gitignore | 1 + Makefile | 38 +++++++++----- acme.conf.5 | 4 +- domains.conf.5 | 19 ++++++- domains.conf.sample | 10 ++++ init.sh | 54 +++++++++++++++++++ kerberos-challenge.sh | 141 -------------------------------------------------- kerberos.sh | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++ lfacme-renew.sh | 38 +++++++------- 9 files changed, 271 insertions(+), 175 deletions(-) delete mode 100644 kerberos-challenge.sh create mode 100644 kerberos.sh diff --git a/.gitignore b/.gitignore index 3268211..89c1dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .*.sw? +/dist diff --git a/Makefile b/Makefile index 1e43e45..f9ffa1c 100644 --- a/Makefile +++ b/Makefile @@ -9,25 +9,27 @@ MAN5DIR?= ${MANDIR}/man5 MAN8DIR?= ${MANDIR}/man8 HOOKDIR?= ${CONFDIR}/hooks -LIBMODE?= 0755 -LIB_FILES= init.sh \ - kerberos-challenge.sh +LIBMODE?= 0644 +LIB= init.sh + +CHALLENGEMODE?= 0755 +CHALLENGE= kerberos.sh BINMODE?= 0755 -BIN_FILES= lfacme-renew.sh \ +BIN= lfacme-renew.sh \ lfacme-setup.sh CONFMODE?= 0644 -CONF_FILES= acme.conf.sample \ +CONF= acme.conf.sample \ domains.conf.sample HOOKMODE?= 0755 -HOOK_FILES= example-hook.sh +HOOK= example-hook.sh MANMODE?= 0644 -MAN5FILES= acme.conf.5 \ +MAN5= acme.conf.5 \ domains.conf.5 -MAN8FILES= lfacme-renew.8 \ +MAN8= lfacme-renew.8 \ lfacme-setup.8 default: all @@ -37,26 +39,34 @@ all: install: @echo 'create ${LIBDIR}'; install -d ${LIBDIR}; \ - for lib in ${LIB_FILES}; do \ + for lib in ${LIB}; do \ echo "install ${LIBDIR}/$$lib"; \ install -C -m ${LIBMODE} "$$lib" "${LIBDIR}/$$lib"; \ done; \ \ + echo 'create ${LIBDIR}/challenge'; install -d ${LIBDIR}/challenge; \ + for challenge in ${CHALLENGE}; do \ + basename=$${challenge%*.sh}; \ + echo "install ${LIBDIR}/challenge/$$basename"; \ + install -C -m ${CHALLENGEMODE} "$$challenge" \ + "${LIBDIR}/challenge/$$basename"; \ + done; \ + \ echo 'create ${BINDIR}'; install -d ${BINDIR}; \ - for bin in ${BIN_FILES}; do \ + for bin in ${BIN}; do \ basename=$${bin%*.sh}; \ echo "install ${BINDIR}/$$basename"; \ install -C -m ${BINMODE} "$$bin" "${BINDIR}/$$basename"; \ done; \ \ echo 'create ${CONFDIR}'; install -d ${CONFDIR}; \ - for conf in ${CONF_FILES}; do \ + for conf in ${CONF}; do \ echo "install ${CONFDIR}/$$conf"; \ install -C -m ${CONFMODE} "$$conf" "${CONFDIR}/$$conf"; \ done; \ \ echo 'create ${HOOKDIR}'; install -d ${HOOKDIR}; \ - for hook in ${HOOK_FILES}; do \ + for hook in ${HOOK}; do \ basename=$${hook%*.sh}; \ echo "install ${HOOKDIR}/$$basename"; \ install -C -m ${HOOKMODE} "$$hook" "${HOOKDIR}/$$basename"; \ @@ -65,13 +75,13 @@ install: echo 'create ${MANDIR}'; install -d ${MANDIR}; \ \ echo 'create ${MAN5DIR}'; install -d ${MAN5DIR}; \ - for man in ${MAN5FILES}; do \ + for man in ${MAN5}; do \ echo "install ${MAN5DIR}/$$man"; \ install -C -m ${MANMODE} "$$man" "${MAN5DIR}/$$man"; \ done; \ \ echo 'create ${MAN8DIR}'; install -d ${MAN8DIR}; \ - for man in ${MAN8FILES}; do \ + for man in ${MAN8}; do \ echo "install ${MAN8DIR}/$$man"; \ install -C -m ${MANMODE} "$$man" "${MAN8DIR}/$$man"; \ done; \ diff --git a/acme.conf.5 b/acme.conf.5 index f03f777..8643d55 100644 --- a/acme.conf.5 +++ b/acme.conf.5 @@ -31,7 +31,9 @@ The default value is .It Va ACME_KERBEROS_PRINCIPAL The Kerberos principal to use when responding to a .Dq dns-01 -challenge. +challenge with the +.Dq kerberos +challenge handler. The default value is .Dq host/$(hostname) . .El diff --git a/domains.conf.5 b/domains.conf.5 index 0f937a6..1ad0e03 100644 --- a/domains.conf.5 +++ b/domains.conf.5 @@ -44,6 +44,23 @@ to generate a secp384r1 ECDSA key, or to generate a 3072-bit RSA key. If not specified, the default value is .Dq ec . +.It Sy challenge Ns Li = Ns Ar filename +Invoke +.Ar filename +to handle ACME challenges for this certificate. +If +.Ar filename +begins with a +.Sq / +character, then it is assumed to be an absolute path, +otherwise it will be searched for in +.Pa /usr/local/share/lfacme/challenge +and +.Pa /usr/local/etc/lfacme/challenge . +.Pp +The challenge script is passed to +.Xr uacme 1 ; +see the uacme documentation for details on the calling convention. .It Sy hook Ns Li = Ns Ar filename Invoke .Ar filename @@ -66,7 +83,7 @@ which may be one of the following: A certificate has been issued or renewed. .El .Pp -The following environment variables will be when running the hook script: +The following environment variables will be set when running the hook script: .Bl -tag -width LFACME_CERTFILE .It Sy LFACME_CERT The identifier of the certificate, i.e. the first field in diff --git a/domains.conf.sample b/domains.conf.sample index 6dace98..41de581 100644 --- a/domains.conf.sample +++ b/domains.conf.sample @@ -31,6 +31,16 @@ # If begins with a '/' then it is an absolute path, # otherwise it is relative to $ACME_HOOKDIR. # This option may be given multiple times. +# +# challenge= +# Use as the challenge handler. If begins +# with '/' then it is an absolute path, otherwise it will +# be searched for in /usr/local/share/lfacme/challenge/ +# then /usr/local/etc/lfacme/challenge/. +# +# One challenge script is supplied with lfacme, "kerberos", +# which uses Kerberized nsupdate(1) to respond to dns-01 +# challenges. # A certificate name of "*" can be used to set the default options for any # following certificates. For example, to use RSA (instead of the default diff --git a/init.sh b/init.sh index 3c9de04..9674bc1 100644 --- a/init.sh +++ b/init.sh @@ -25,6 +25,7 @@ _warn() { _BASEDIR="/usr/local" # Where the internal scripts are. _SHARE="${_BASEDIR}/share/lfacme" +_CHALLENGE="${_SHARE}/challenge" # Our configuration directory. This might be overridden by command-line # arguments. @@ -71,3 +72,56 @@ _UACME=/usr/local/bin/uacme _uacme() { "$_UACME" -a "$ACME_URL" -c "$_UACME_DIR" "$@" } + +# Find a challenge script and make sure it's valid. If the challenge name +# begins with a '/' it's a full path, otherwise we search $_CHALLENGE and +# $_CONFDIR/challenge. +_findchallenge() { + local identifier="$1" + local challenge="$2" + local path="" + + if [ "${challenge#/*}" != "$challenge" ]; then + path="${challenge}" + elif [ -f "${_CHALLENGE}/${challenge}" ]; then + path="${_CHALLENGE}/${challenge}" + elif [ -f "${_CONFDIR}/challenge/${challenge}" ]; then + path="${_CONFDIR}/challenge/${challenge}" + else + _error "%s: could not find challenge script '%s'" \ + "$identifier" "$challenge" + return 1 + fi + + if ! [ -x "$path" ]; then + _error "%s: challenge is not executable: %s" \ + "$identifier" "$path" + return 1 + fi + + echo "$path" +} + +# Find a hook script and make sure it's valid. If the hook name begins with a +# '/' it's a full path, otherwise it's relative to ACME_HOOKDIR. +_findhook() { + hook="$1" + + if [ "${hook#/*}" = "$hook" ]; then + hook="${ACME_HOOKDIR}/$hook" + fi + + if ! [ -f "$hook" ]; then + _error "%s: hook does not exist: %s" \ + "$identifier" "$hook" + return 1 + fi + + if ! [ -x "$hook" ]; then + _error "%s: hook is not executable: %s" \ + "$identifier" "$hook" + return 1 + fi + + echo "$hook" +} diff --git a/kerberos-challenge.sh b/kerberos-challenge.sh deleted file mode 100644 index bd9d9e4..0000000 --- a/kerberos-challenge.sh +++ /dev/null @@ -1,141 +0,0 @@ -#! /bin/sh -# This source code is released into the public domain. - -. /usr/local/share/lfacme/init.sh - -# begin, done or failed -ACTION=$1 -# ACME method, must be dns-01. -METHOD=$2 -# This is the full domain name we're authorising. -DOMAIN=$3 -# Token name, not used for dns-01. -TOKEN=$4 -# The token value we need to create. -AUTH=$5 - -if [ "$#" -ne 5 ]; then - _fatal "missing arguments" -fi - -if [ "$METHOD" != "dns-01" ]; then - _warn "skip method %s" "$METHOD" - exit 1 -fi - -if ! kinit -k -t /etc/krb5.keytab "$ACME_KERBEROS_PRINCIPAL"; then - _fatal "failed to obtain a Kerberos ticket" -fi - -# Keep removing labels from the name until we find one with nameservers. -_getnameservers() { - local domain="$1" - - local _trydomain="$domain" - while ! [ -z "$_trydomain" ]; do - if [ "$_trydomain" = "${_trydomain#*.}" ]; then - # If there are no dots in the domain, we couldn't - # find the nameservers. - break - fi - - # For CNAME records, a query for NS will return the CNAME. - # Therefore we have to check we actually got NS records. - local nameservers="$( - dig "$_trydomain" ns +noall +answer | \ - awk '$4 == "NS" { print $5 }' - )" - - if ! [ -z "$nameservers" ]; then - echo "$nameservers" - return - fi - - _trydomain="${_trydomain#*.}" - done - - _fatal "unable to find nameservers for %s" "$_trydomain" -} - -# Add a new record using nsupdate. -_add_record() { - local domain="$1" - local auth="$2" - - nsupdate -g <