edgebsd

#!/bin/sh
#Copyright (c) 2021-2023 Pierre Pronchery <khorben@edgebsd.org>
#
#This code is derived from software contributed to the EdgeBSD Project
#by Pierre Pronchery <khorben@edgebsd.org>
#
#Redistribution and use in source and binary forms, with or without
#modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
#DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
#FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
#DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
#SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#variables
#executables
ACME_TINY="@PREFIX@/bin/acme-tiny-@PYVERSSUFFIX@"
ACME="$ACME_TINY"
CAT="cat"
CHMOD="chmod"
DEBUG="_debug"
MKDIR="mkdir -p"
MKTEMP="mktemp"
MV="mv -f"
OPENSSL="openssl"
RM="rm -f"
SERVICE="/usr/sbin/service"
TOUCH="touch"
#settings
PREFIX="@PREFIX@"
PROGNAME="edgebsd-tls"
SYSCONFDIR="@SYSCONFDIR@"
VENDOR="EdgeBSD"
VERBOSE=1
ACCOUNTKEY="account.key"
ACMEDIR="/var/www/acme"
BASEDIR="$SYSCONFDIR/$VENDOR/$PROGNAME"
OPENSSLARGS=
OPENSSLCNF="$SYSCONFDIR/$VENDOR/$PROGNAME/openssl.cnf"
[ ! -f "$OPENSSLCNF" -a -d "/etc/ssl" ] && OPENSSLCNF="/etc/ssl/openssl.cnf"
[ ! -f "$OPENSSLCNF" -a -d "/etc/openssl" ] &&
OPENSSLCNF="/etc/openssl/openssl.cnf"
OPENSSL_DAYS=365
OPENSSL_KEY_SIZE=4096
RENEW_HOSTS=
RENEW_SERVICES=
#load local settings
[ -f "$SYSCONFDIR/$VENDOR/$PROGNAME.conf" ] &&
. "$SYSCONFDIR/$VENDOR/$PROGNAME.conf"
[ -n "$HOME" -a -f "$HOME/.config/$VENDOR/$PROGNAME.conf" ] &&
. "$HOME/.config/$VENDOR/$PROGNAME.conf"
#functions
#debug
_debug()
{
[ $VERBOSE -ge 2 ] && echo "$@" 1>&2
"$@"
}
#error
_error()
{
echo "$PROGNAME: $@" 1>&2
return 2
}
#info
_info()
{
echo "$PROGNAME: $@"
return 0
}
#edgebsd-tls
_edgebsd_tls()
{
shift $((OPTIND - 1))
if [ $# -eq 0 ]; then
_usage
return $?
fi
command="$1"
shift
case "$command" in
acme|create|renew)
"_edgebsd_tls_$command" "$@"
return $?
;;
*)
_usage "$command: Unknown command"
return $?
;;
esac
}
_edgebsd_tls_acme()
{
ret=0
while getopts "O:qv" name; do
case "$name" in
O)
eval "${OPTARG%%=*}='${OPTARG#*=}'"
;;
q)
ACME="$ACME_TINY --quiet"
VERBOSE=0
;;
v)
ACME="$ACME_TINY"
VERBOSE=$((VERBOSE + 1))
;;
*)
_usage
return $?
;;
esac
done
shift $((OPTIND - 1))
if [ $# -eq 0 ]; then
_usage "$PROGNAME: At least one hostname is required"
return $?
fi
for hostname in "$@"; do
_acme_host "$hostname" || ret=2
done
return $ret
}
_acme_host()
{
keyfile="$BASEDIR/$ACCOUNTKEY"
hostname="$1"
[ -d "$BASEDIR" ] || $DEBUG $MKDIR -- "$BASEDIR"
if [ ! -f "$keyfile" ]; then
[ $VERBOSE -ge 2 ] && _info "Creating the account key..."
$DEBUG $TOUCH "$keyfile" &&
$DEBUG $CHMOD 0600 "$keyfile" &&
$DEBUG $OPENSSL genrsa -out "$keyfile" 4096
if [ $? -ne 0 ]; then
_error "$keyfile: Could not create the account key"
return $?
fi
fi
[ -d "$ACMEDIR" ] || $DEBUG $MKDIR -- "$ACMEDIR"
tmpfile=$($DEBUG $MKTEMP)
[ $? -eq 0 ] || return 2
$DEBUG $ACME --account-key "$keyfile" \
--csr "$BASEDIR/$hostname.csr" \
--acme-dir "$ACMEDIR" > "$tmpfile" &&
$DEBUG $CAT "$tmpfile" > "$BASEDIR/$hostname.crt"
res=$?
$DEBUG $RM -- "$tmpfile"
return $res
}
_edgebsd_tls_create()
{
ret=0
bits="rsa:$OPENSSL_KEY_SIZE"
days=$OPENSSL_DAYS
ext=".csr"
extensions="-extensions v3_req"
force=0
sign=
user=0
while getopts "O:b:d:fqsuv" name; do
case "$name" in
O)
eval "${OPTARG%%=*}='${OPTARG#*=}'"
;;
b)
bits="rsa:$OPTARG"
;;
d)
#XXX verify that OPTARG is numerical
days="$OPTARG"
;;
f)
force=1
;;
q)
VERBOSE=0
;;
s)
sign="-x509"
ext=".crt"
[ $user -eq 0 ] && extensions="-extensions srv_cert"
;;
u)
user=1
extensions="-extensions usr_cert"
;;
v)
VERBOSE=$((VERBOSE + 1))
;;
*)
_usage
return $?
;;
esac
done
shift $((OPTIND - 1))
if [ $# -eq 0 ]; then
_usage "create: At least one hostname is required"
return $?
fi
[ -f "$OPENSSLCNF" ] && OPENSSLARGS="-config $OPENSSLCNF"
[ -n "$sign" -a $days -gt 0 ] && sign="$sign -days $days"
for hostname in "$@"; do
_create_hostname "$hostname" "$extensions" || ret=2
done
return $ret
}
_create_hostname()
{
hostname="$1"
extensions="$2"
s="$sign"
[ $user -eq 0 ] && case "$hostname" in
www.*)
[ $VERBOSE -ge 2 ] && _info "${hostname#www.}:" \
" Adding alternate subject..."
s="$s -addext subjectAltName=DNS:${hostname#www.}"
;;
esac
keyfile="$hostname.key"
[ $VERBOSE -ge 2 ] && _info "$keyfile: Generating private key..."
if [ -f "$keyfile" ]; then
if [ $force -eq 0 ]; then
_error "$hostname: Private key already generated"
return $?
fi
else
$DEBUG $TOUCH "$keyfile" &&
$DEBUG $CHMOD 0600 "$keyfile"
if [ $? -ne 0 ]; then
_error "$keyfile: Could not create the private key"
return $?
fi
fi
[ $VERBOSE -ge 2 ] && _info "$hostname$ext: Creating certificate..."
$DEBUG $OPENSSL req $OPENSSLARGS -new $s -subj "/CN=$hostname" \
-nodes -sha256 -newkey "$bits" -keyout "$keyfile" $extensions \
-out "$hostname$ext"
if [ $? -ne 0 ]; then
_error "$hostname: Could not create the certificate"
return $?
fi
}
_edgebsd_tls_renew()
{
ret=0
all=0
reload=0
services=
while getopts "O:S:aqv" name; do
case "$name" in
O)
eval "${OPTARG%%=*}='${OPTARG#*=}'"
;;
S)
services="$OPTARG"
;;
a)
all=1
;;
q)
VERBOSE=0
;;
v)
VERBOSE=$((VERBOSE + 1))
;;
*)
_usage
return $?
;;
esac
done
[ -n "$services" ] || services="$RENEW_SERVICES"
shift $((OPTIND - 1))
if [ $# -eq 0 -a $all -ne 0 ]; then
for hostname in $RENEW_HOSTS; do
_renew_host "$hostname"
if [ $? -eq 0 ]; then
reload=1
else
ret=2
fi
done
elif [ $# -ne 0 ]; then
for hostname in "$@"; do
_renew_host "$hostname"
if [ $? -eq 0 ]; then
reload=1
else
ret=2
fi
done
else
_usage "renew: At least one hostname is required"
return $?
fi
[ $reload -eq 1 ] && for service in $services; do
[ $VERBOSE -ge 2 ] && _info "$service: Reloading service..."
($DEBUG $SERVICE "$service" reload ||
$DEBUG $SERVICE "$service" restart) || ret=3
done
return $ret
}
_renew_host()
{
hostname="$1"
[ $VERBOSE -ge 2 ] && _info "$hostname: Renewing host..."
_acme_host "$hostname"
}
#usage
_usage()
{
[ $# -ge 1 ] && echo "$PROGNAME: $@" 1>&2
echo "Usage: $PROGNAME command [options...]" 1>&2
echo 1>&2
echo "Commands supported:" 1>&2
echo " acme Request one or more TLS certificate signatures" 1>&2
echo " create Create one or more TLS certificates" 1>&2
echo " renew Renew one or more TLS certificates" 1>&2
echo 1>&2
echo " $PROGNAME acme [-qv] hostname..." 1>&2
echo 1>&2
echo " $PROGNAME create [-b bits][-d days][-fqsv] hostname..." 1>&2
echo " $PROGNAME create [-b bits][-d days][-fqsv] -u e-mail..." 1>&2
echo " -b Size of the key to generate (default: $OPENSSL_KEY_SIZE)" 1>&2
echo " -d Duration of the validity in days (default: $OPENSSL_DAYS)" 1>&2
echo " -q Operate in quiet mode" 1>&2
echo " -s Generate a self-signed certificate" 1>&2
echo " -u Generate a user certificate" 1>&2
echo " -v Increase the verbosity" 1>&2
echo 1>&2
echo " $PROGNAME renew [-S service...][-qv] hostname..." 1>&2
echo " $PROGNAME renew [-S service...][-qv] -a" 1>&2
echo " -S List of services to reload upon success" 1>&2
echo " -q Operate in quiet mode" 1>&2
echo " -v Increase the verbosity" 1>&2
echo " -a Renew all the hosts configured" 1>&2
return 1
}
#main
_edgebsd_tls "$@"