#!/bin/bash
# OpenSLX SSL Certificate management
mkdir -p "/run/openslx"
if ! mkdir "/run/openslx/cert-manager" 2> /dev/null; then
echo "Already in progress."
exit 1
fi
trap 'rm -rf -- /run/openslx/cert-manager' EXIT
declare -rg BASE="/etc/ssl/openslx"
declare -rg PRIVDIR="$BASE/private"
declare -rg CERTDIR="$BASE/cert"
declare -rg LIGHTDIR="$BASE/lighttpd"
mkdir -p "$BASE" "$PRIVDIR" "$CERTDIR"
chown -R root:root "$BASE" || exit 1
chmod u+rwx,go+rx-w "$BASE" "$CERTDIR" || exit 1
chmod u+rwx,go-rwx "$PRIVDIR" || exit 1
# Before doing anything, make sure we have a CA with enough validity left
# File name format for ca is:
# ${PRIVDIR}/ca-TTTTTTTTTT.key
# ${CERTDIR}/ca-TTTTTTTTTT.crt
# Where TT is the unix timestamp of "validTo" of that cert.
#
# A new CA will be generated when the previous has less than two years
# of validity left.
# We deliver a new CA to clients immediately when it was generated, but
# keep signing server certs with the old one, until the old one only
# has a year of validity left, then we switch to the next-oldest CA-cert.
# CA certs are valid for 10 years, server certs for 1 year.
# This should allow for clients to have two years of uptime without any
# problems due to cert rollover.
declare -rg NOW="$( date +%s )"
# PROD
declare -rg srv_days=365 # 1y
declare -rg srv_s="$(( srv_days * 86400 ))"
declare -rg srv_min_remain_s="$(( srv_days / 2 * 86400 ))" # half a year
declare -rg srv_new_ts="$(( srv_days * 86400 + NOW ))"
declare -rg ca_days="$(( 10 * 365 ))" # 10y
declare -rg ca_min_remain_s="$(( 2 * srv_days * 86400 ))" # 2y, twice that of a server cert
declare -rg ca_new_expire_ts="$(( ca_days * 86400 + NOW ))"
# TEST
#declare -rg ca_days=1825 # 5y
#declare -rg ca_min_remain_s="$(( 1260 ))" # bit more than 1y
#declare -rg ca_new_expire_ts="$(( 1320 + NOW ))"
#declare -rg srv_days=365 # 1y
#declare -rg srv_min_remain_s="$(( 1200 ))" # half a year
#declare -rg srv_new_ts="$(( 1230 + NOW ))"
# Extract timestamp from given filename.
# Filename format has to be ca-UUUUUUUUU[.-]* or srv-UUUUUUUU[.-]*
# Where UUUUU is a unix timestamp
get_ts () {
ts="${1%.*}" # Remove everything from last dot on
ts="${ts##*/ca-}" # remove ca- prefix
ts="${ts##*/srv-}" # remove srv- prefix
from="${ts%-*}" # Remove everything from last dash on
if [ "$from" = "$ts" ]; then
# There was no dash, we have a normal timestamp
from=
else
# There was a dash - $from is now the start of the range,
# $ts is the end of the range
ts="${ts#*-}"
fi
# Set the return value
(( ts > 1234567890 ))
}
# Create an openssl config for signing CSRs
create_conf () {
ca_dir="$( mktemp -d /tmp/bwlp-XXXXXXXX )"
[ -z "$ca_dir" ] && exit 1
mkdir "$ca_dir"/{certs,crl,newcerts,private}
touch "$ca_dir"/index.txt
ca_config="$ca_dir/openssl.cnf"
cp -f "/etc/ssl/openssl.cnf" "$ca_config"
cat >> "$ca_config" <<-MYCA
[ CA_openslx ]
dir = $ca_dir
certs = \$dir/certs
crl_dir = \$dir/crl
database = \$dir/index.txt
new_certs_dir = \$dir/newcerts
serial = \$dir/serial
crl = \$dir/crl.pem
x509_extensions = usr_cert
name_opt = ca_default
cert_opt = ca_default
default_md = default
preserve = no
policy = policy_match
MYCA
}
declare -ag cmdargs=( "$@" )
maybe_restart () {
for i in "${cmdargs[@]}"; do
[ "$i" = "--restart" ] && exit 13
done
echo "Restating script, wiping everything except CAs..."
rm -f -- /etc/ssl/openslx/*/{srv,int}*
exec "$0" "${cmdargs[@]}" --restart
exit 14
}
# Start
# Check if existing CA is still valid for long enough
# Globbing is sorted so should make sure we check the newest one last
declare -a ca_list=()
ca_last=
latest_ca_cert=
oldest_ca_ts=
for i in "${PRIVDIR}"/ca-??????????.key; do
[ -s "$i" ] || continue
if ! get_ts "$i"; then
echo "Invalid ca-key: $i"
rm -f -- "$i"
continue
fi
cert="${CERTDIR}/ca-${ts}.crt"
if ! [ -s "$cert" ] \
|| (( ts < NOW )); then
# Missing cert, or expired -> delete
rm -f -- "$cert" "$i"
continue
fi
# Check if key and cert match
if [ "$( openssl x509 -in "$cert" -noout -pubkey )" != "$( openssl pkey -in "$i" -pubout )" ]; then
echo "Publickey in cert doesn't match publickey in keypair: $cert + $i"
rm -f -- "$cert" "$i"
continue
fi
# All CAs still valid
ca_list+=( "$cert" )
# Latest CA
ca_last="$ts"
latest_ca_cert="$cert"
# Oldest one that is still valid for as long as we want to sign a new cert
if [ -z "$oldest_ca_ts" ] && (( ts > NOW + srv_s )); then
oldest_ca_ts="$ts"
fi
done
mknew=
if [ -z "$ca_last" ] || (( ca_last < NOW + ca_min_remain_s )); then
# Make new CA since the newest one we have is about to expire
echo "Creating new CA..."
cert="${CERTDIR}/ca-${ca_new_expire_ts}.crt"
openssl req -new -newkey rsa:3072 -x509 -days "$ca_days" -extensions v3_ca \
-nodes -subj "/C=DE/ST=PewPew/L=HeyHey/O=bwLehrpool/CN=ca-${NOW}.bwlehrpool" \
-keyout "${PRIVDIR}/ca-${ca_new_expire_ts}.key" -out "$cert" || exit 2
ca_list+=( "$cert" )
ca_last="${ca_new_expire_ts}"
latest_ca_cert="$cert"
if [ -z "$oldest_ca_ts" ]; then
oldest_ca_ts="$ca_new_expire_ts"
fi
mknew=1
fi
# Repackage config.tgz module?
if [ -n "$mknew" ] || ! [ -s "/opt/openslx/configs/modules/self-signed-ca.tar" ] \
|| ! [ -e "/run/openslx/cert-conf-done" ] \
|| [ "/opt/openslx/configs/modules/self-signed-ca.tar" -ot "$latest_ca_cert" ]; then
# Rebuild config module for clients
echo "Updating client config module..."
(
tmpdir="$( mktemp -d '/tmp/bwlp-XXXXXXX' )"
# Copy all current ca-certs to tmpdir
cp -a "${CERTDIR}/"ca-*.crt "$tmpdir/"
cd "$tmpdir/" || exit 6
# Build hashed symlinks for openssl
openssl rehash .
# Put everything in config module and rebuild
tar -c -k -f "/opt/openslx/configs/modules/self-signed-ca.tar" \
--transform 's#^[./][./]*#/opt/openslx/ssl/#' .
cd /tmp || exit 7
rm -rf -- "$tmpdir"
sudo -u www-data -n php /srv/openslx/www/slx-admin/api.php sysconfig --action rebuild
echo "."
touch "/run/openslx/cert-conf-done"
)
fi
# Now check the server certificate
# First, check if we could still use old-style certs with intermediate
declare -a unt_list=()
for i in "${CERTDIR}"/intermediate-??????????.crt; do
[ -s "$i" ] || continue
if ! get_ts "$i"; then
echo "Invalid intermediate cert: $i"
rm -f -- "$i"
continue
fi
if (( ts < NOW )); then
echo "Expired intermediate: $i"
rm -f -- "$i"
continue
fi
unt_list+=( "-untrusted" "$i" )
done
# Now check existing server certs
have_srv=
for i in "${PRIVDIR}"/srv-??????????.key; do
[ -s "$i" ] || continue
if ! get_ts "$i"; then
echo "Invalid srv-key: $i"
rm -f -- "$i"
continue
fi
cert="${CERTDIR}/srv-${ts}.crt"
if (( ts < NOW + srv_min_remain_s )) || ! [ -s "$cert" ]; then
echo "Expired srv cert or key with no cert: $i"
rm -f -- "$i" "$cert"
continue
fi
# Keys match?
if [ "$( openssl x509 -in "$cert" -noout -pubkey )" != "$( openssl pkey -in "$i" -pubout )" ]; then
echo "Publickey in cert doesn't match publickey in keypair: $cert + $i"
rm -f -- "$cert" "$i"
continue
fi
# Validate chain
valid=
for ca in "${ca_list[@]}"; do
if openssl verify -CAfile "$ca" "${unt_list[@]}" \
"$cert" &> /dev/null; then
valid=1
break
fi
done
if [ -n "$valid" ]; then
have_srv=1
break
fi
echo "No valid CA/chain for $i, removing"
rm -f -- "$i" "$cert"
done
# Now still check the current lighttpd config, in case it is out of sync
# with our generated stuff for whatever reason.
if [ -n "$have_srv" ] || [ -z "$makenew" ]; then
if [ -s "${LIGHTDIR}/ca-chain.pem" ]; then
unt_list=( "-untrusted" "${LIGHTDIR}/ca-chain.pem" )
else
unt_list=()
fi
valid=
for ca in "${ca_list[@]}"; do
openssl verify -CAfile "$ca" "${unt_list[@]}" \
"${LIGHTDIR}/server.pem" &> /dev/null || continue
valid=1
break
done
if [ -z "$valid" ]; then
echo "Current lighttpd SSL setup seems invalid, making new one"
makenew=1
fi
fi
# Make new one?
if [ -z "$have_srv" ] || [ -n "$makenew" ]; then
# Request ServerCert
csr="$( mktemp /tmp/bwlp-XXXXXXX.csr )"
echo "Generating new Server Certificate. Key+CSR..."
rm -f -- "${CERTDIR}"/srv-*.crt "${PRIVDIR}/srv.key.tmp" "${PRIVDIR}"/srv-*.key
openssl req -newkey rsa:3072 -nodes -keyout "${PRIVDIR}/srv.key.tmp" -out "$csr" \
-subj "/C=DE/ST=PewPew/L=HeyHey/O=bwLehrpool/CN=satellite.bwlehrpool" || exit 4
echo "Signing Server Certificate with CA..."
sign_cert="${CERTDIR}/ca-${oldest_ca_ts}.crt"
sign_key="${PRIVDIR}/ca-${oldest_ca_ts}.key"
if ! [ -s "$sign_cert" ] || ! [ -s "$sign_key" ]; then
echo "CA sign/key to sign does not exist!?"
maybe_restart
fi
echo "Signing with $sign_cert"
create_conf
# Need extfile for SAN, chromium doesn't honor CN anymore
cat > "${csr}.cnf" <<-END
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "bwLehrpool Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = satellite.bwlehrpool
END
if ! openssl ca -config "$ca_config" -create_serial -policy policy_anything -days "$srv_days" \
-cert "$sign_cert" -keyfile "$sign_key" -extfile "${csr}.cnf" \
-notext -name CA_openslx -batch -out "${CERTDIR}/srv-${srv_new_ts}.crt" -in "$csr"; then
echo "Failed to sign CSR"
rm -f -- "$sign_key" "$sign_cert"
maybe_restart
fi
rm -rf -- "$ca_dir"
rm -f -- "$csr" "${csr}.cnf"
mv "${PRIVDIR}/srv.key.tmp" "${PRIVDIR}/srv-${srv_new_ts}.key" || exit 5
# Combine and prepare for lighttpd
mkdir -p "$LIGHTDIR" || exit 10
# Combine cert and key, as required by (older) lighttpd
echo "Writing out lighttpd PEMs..."
cat "${CERTDIR}/srv-${srv_new_ts}.crt" "${PRIVDIR}/srv-${srv_new_ts}.key" > "${LIGHTDIR}/server.pem" || exit 10
chmod 0600 "${LIGHTDIR}/server.pem"
# Don't need this anymore
rm -f -- "${LIGHTDIR}/ca-chain.pem"
if [ "$1" = "--restart" ] || [ -t 0 ]; then
echo "Restarting lighttpd..."
systemctl restart lighttpd.service
fi
fi
echo "Done."
exit 0