From debc7d7160101d4be569aca827e0e4f5791d7c0e Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 20 Sep 2023 15:14:35 +0200 Subject: [SSPS] Rewrite CA/cert generator and handling Better sanity checks and fallbacks. --- .../static_files/lighttpd/opt/openslx/slx-cert | 302 +++++++++++++-------- 1 file changed, 196 insertions(+), 106 deletions(-) diff --git a/satellit_installer/static_files/lighttpd/opt/openslx/slx-cert b/satellit_installer/static_files/lighttpd/opt/openslx/slx-cert index e25e3d7..3409244 100755 --- a/satellit_installer/static_files/lighttpd/opt/openslx/slx-cert +++ b/satellit_installer/static_files/lighttpd/opt/openslx/slx-cert @@ -2,44 +2,47 @@ # OpenSLX SSL Certificate management -if ! mkdir "/run/openslx-cert-manager"; then +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 +trap 'rm -rf -- /run/openslx/cert-manager' EXIT declare -rg BASE="/etc/ssl/openslx" -declare -rg PRIV="$BASE/private" -declare -rg CERT="$BASE/cert" -declare -rg LIGHT="$BASE/lighttpd" +declare -rg PRIVDIR="$BASE/private" +declare -rg CERTDIR="$BASE/cert" +declare -rg LIGHTDIR="$BASE/lighttpd" -mkdir -p "$BASE" "$PRIV" "$CERT" +mkdir -p "$BASE" "$PRIVDIR" "$CERTDIR" chown -R root:root "$BASE" || exit 1 -chmod u+rwx,go+rx-w "$BASE" "$CERT" || exit 1 -chmod u+rwx,go-rwx "$PRIV" || 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: -# ${PRIV}/ca-FFFFFFFFFF-TTTTTTTTTT.key -# ${CERT}/ca-TTTTTTTTTT.crt -# Where TT is the unix timestamp of "validTo" of that cert -# And FF is the unix timestamp of when we should starting using a CA to -# sign our certificates. This is for a grace period between CA certs. -# We deliver a new CA certificate immediately when it was generated, but -# only start signing server certificates with it after a grace period of -# 180 days. Any client that rebooted within those 180 days will not run -# into any certificate issues, but if you wanted to cover that case too -# you could make it so the client re-downloads trusted CA-certs every -# couple days. +# ${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 ca_days="$(( 10 * 365 ))" # 10y -declare -rg ca_min_remain_s="$(( 400 * 86400 ))" # bit more than 1y -declare -rg ca_new_expire_ts="$(( ca_days * 86400 + NOW ))" declare -rg srv_days=365 # 1y -declare -rg srv_min_remain_s="$(( 180 * 86400 ))" # half a year +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 @@ -49,18 +52,27 @@ declare -rg srv_new_ts="$(( srv_days * 86400 + NOW ))" #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%.*}" - ts="${ts##*/ca-}" - ts="${ts##*/srv-}" - from="${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 @@ -86,121 +98,196 @@ create_conf () { MYCA } -latest_ca_file= +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= -for i in "${PRIV}"/ca-??????????.key; do +latest_ca_cert= +oldest_ca_ts= +for i in "${PRIVDIR}"/ca-??????????.key; do [ -s "$i" ] || continue - get_ts "$i" - if ! [ -s "${CERT}/ca-${ts}.crt" ] \ - || ! [ -s "${CERT}/intermediate-${ts}.crt" ] \ - || ! [ -s "${PRIV}/intermediate.key" ] \ + 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}/ca-${ts}.crt" "${PRIV}/ca-${ts}.key" "${CERT}/intermediate-${ts}.crt" + 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_file="${CERT}/ca-${ts}.crt" + 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" ] || (( NOW + ca_min_remain_s > ca_last )); then - # Make new CA +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..." - openssl req -new -newkey rsa:4096 -x509 -days "$ca_days" -extensions v3_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 "${PRIV}/ca-${ca_new_expire_ts}.key" -out "${CERT}/ca-${ca_new_expire_ts}.crt" || exit 2 + -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 - # - # Create new intermediate, sign with all CAs - csr="$( mktemp /tmp/bwlp-XXXXXXX.csr )" - # Create request, CA:TRUE - echo "Generate intermediate key+CSR..." - [ -s "${PRIV}/intermediate.key" ] || openssl genrsa -out "${PRIV}/intermediate.key" 4096 - openssl req -new -key "${PRIV}/intermediate.key" \ - -nodes -subj "/C=DE/ST=PewPew/L=HeyHey/O=bwLehrpool/CN=intermediate.bwlehrpool" \ - -out "$csr" || exit 2 - create_conf - # Sign request, CA:TRUE - echo "Sign new intermediate key with CA..." - openssl ca -config "$ca_config" -extensions v3_ca -create_serial \ - -policy policy_anything -days "$ca_days" \ - -cert "${CERT}/ca-${ca_new_expire_ts}.crt" -keyfile "${PRIV}/ca-${ca_new_expire_ts}.key" \ - -notext -name CA_openslx -batch -out "${CERT}/intermediate-${ca_new_expire_ts}.crt" -in "$csr" || exit 2 - rm -rf -- "$ca_dir" "$csr" fi - +# Repackage config.tgz module? if [ -n "$mknew" ] || ! [ -s "/opt/openslx/configs/modules/self-signed-ca.tar" ] \ - || [ "/opt/openslx/configs/modules/self-signed-ca.tar" -ot "$latest_ca_file" ]; then + || ! [ -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' )" - cp -a "${CERT}/"ca-*.crt "$tmpdir/" + # 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 -declare -a srv_list -srv_list=() -for i in "${PRIV}"/srv-??????????.key; do +# Now check existing server certs +have_srv= +for i in "${PRIVDIR}"/srv-??????????.key; do [ -s "$i" ] || continue - get_ts "$i" - if (( ts < NOW )) || ! [ -s "${CERT}/srv-${ts}.crt" ]; then - rm -f -- "$i" "${CERT}/srv-${ts}.crt" + 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 - srv_list+=( "$ts" ) + # 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 -if [ -n "$mknew" ] || [ "${#srv_list[@]}" = 0 ] \ - || [ "$(( NOW + srv_min_remain_s ))" -gt "${srv_list[-1]}" ]; then +# 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 -- "${CERT}"/srv-*.crt "${PRIV}/srv.key.tmp" "${PRIV}"/srv-*.key - openssl req -new -nodes -keyout "${PRIV}/srv.key.tmp" -out "$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 intermediate..." - declare -a in_list - in_list=() - for i in "${CERT}"/intermediate-??????????.crt; do - [ -s "$i" ] || continue - get_ts "$i" - if (( ts < NOW )); then - echo "Expired intermediate $i" - rm -f -- "$i" - continue - fi - echo "Have intermediate $i" - in_list+=( "$i" ) - done - if [ "${#in_list[@]}" = 0 ]; then - echo "ERROR: Have no intermediate certificate" - exit 11 - fi - for in_cert in "${in_list[@]}"; do - get_ts "$in_cert" - (( ts < 30 * 86400 + NOW )) && continue # Expiring in a month, ignore - break # Need only one really - done - echo "Signing with $in_cert" + 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 = "OpenSSL Generated Server Certificate" + nsComment = "bwLehrpool Generated Server Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always keyUsage = critical, digitalSignature, keyEncipherment @@ -209,25 +296,28 @@ if [ -n "$mknew" ] || [ "${#srv_list[@]}" = 0 ] \ [alt_names] DNS.1 = satellite.bwlehrpool END - openssl ca -config "$ca_config" -create_serial -policy policy_anything -days "$srv_days" \ - -cert "$in_cert" -keyfile "${PRIV}/intermediate.key" -extfile "${csr}.cnf" \ - -notext -name CA_openslx -batch -out "${CERT}/srv-${srv_new_ts}.crt" -in "$csr" || exit 4 + 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 "${PRIV}/srv.key.tmp" "${PRIV}/srv-${srv_new_ts}.key" || exit 5 - srv_list+=( "$srv_new_ts" ) + mv "${PRIVDIR}/srv.key.tmp" "${PRIVDIR}/srv-${srv_new_ts}.key" || exit 5 # Combine and prepare for lighttpd - mkdir -p "$LIGHT" || exit 10 + mkdir -p "$LIGHTDIR" || exit 10 - # Combine cert and key, as required by lighttpd + # Combine cert and key, as required by (older) lighttpd echo "Writing out lighttpd PEMs..." - cat "${CERT}/srv-${srv_new_ts}.crt" "${PRIV}/srv-${srv_new_ts}.key" > "${LIGHT}/server.pem" || exit 10 - chmod 0600 "${LIGHT}/server.pem" + cat "${CERTDIR}/srv-${srv_new_ts}.crt" "${PRIVDIR}/srv-${srv_new_ts}.key" > "${LIGHTDIR}/server.pem" || exit 10 + chmod 0600 "${LIGHTDIR}/server.pem" - # Create ca-chain - cat "${in_list[@]}" > "${LIGHT}/ca-chain.pem" + # Don't need this anymore + rm -f -- "${LIGHTDIR}/ca-chain.pem" if [ "$1" = "--restart" ] || [ -t 0 ]; then echo "Restarting lighttpd..." -- cgit v1.2.3-55-g7522