#!/bin/sh # Sourced by the init script after functions.sh. Only runtime paths are changed; # empty UCI certificate paths continue to select automatic certificates. # The && chains deliberately fail if any preparation command fails. # shellcheck disable=SC2034,SC2154,SC2015 OPENLIST2_TLS_TIMEOUT=60 OPENLIST2_TLS_RENEW_BEFORE=2592000 openlist2_tls_run() { timeout -k 1 "$OPENLIST2_TLS_TIMEOUT" "$@" /dev/null)" || protected="$path" for directory in "$temp_dir" "$bleve_dir" "$dist_dir"; do [ -n "$directory" ] || continue if openlist2_contains_path "$directory" "$protected"; then openlist2_error "TLS files must not be inside cache, index or public frontend directories." return 1 fi done done return 0 } openlist2_tls_hostname() { local hostname hostname="$(uci -q get 'system.@system[0].hostname')" case "$hostname" in ''|*[!A-Za-z0-9.-]*) hostname=openlist ;; esac if [ "${#hostname}" -gt 253 ] || ! printf '%s\n' "$hostname" | grep -Eq '^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$'; then hostname=openlist fi # Hostnames above are ASCII; minimal BusyBox tr may omit character classes. # shellcheck disable=SC2018,SC2019 printf '%s\n' "$hostname" | LC_ALL=C tr 'A-Z' 'a-z' } openlist2_tls_lan_ip() ( local address ip set -f # OpenWrt accepts a list of LAN addresses, optionally with CIDR suffixes. # shellcheck disable=SC2046 for address in $(uci -q get network.lan.ipaddr); do ip="$(printf '%s\n' "${address%%/*}" | LC_ALL=C awk -F. ' NF != 4 { exit 1 } { for (i = 1; i <= 4; i++) if ($i !~ /^[0-9]+$/ || length($i) > 3 || $i + 0 > 255) exit 1 printf "%d.%d.%d.%d\n", $1, $2, $3, $4 } ')" || continue printf '%s\n' "$ip" exit 0 done exit 0 ) openlist2_tls_pair_valid() ( local cert="$1" key="$2" min_lifetime="${3:-0}" hostname="${4:-}" ip="${5:-}" local work_dir cert_pub key_pub actual_san expected_san umask 077 work_dir="$(mktemp -d "${TMPDIR:-/tmp}/openlist2-tls-check.XXXXXX")" || exit 1 trap 'rm -rf "$work_dir"' 0 trap 'exit 1' HUP INT TERM # Arguments are expanded by the child shell, inside the timeout. # shellcheck disable=SC2016 openlist2_tls_run /bin/sh -c ' [ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ] && [ -f "$2" ] && [ -r "$2" ] && [ -s "$2" ] ' openlist2-tls-check "$cert" "$key" 2>/dev/null && openlist2_tls_run openssl x509 -in "$cert" -noout -pubkey -checkend "$min_lifetime" >"$work_dir/cert" 2>/dev/null && openlist2_tls_run openssl pkey -in "$key" -pubout >"$work_dir/key.pub" 2>/dev/null && openlist2_tls_run openssl verify -purpose sslserver -partial_chain -trusted "$cert" "$cert" >/dev/null 2>&1 || exit 1 # Trusting the configured leaf here checks its dates and server purpose; # it does not install trust into the router or any browser. cert_pub="$(sed -n '/^-----BEGIN PUBLIC KEY-----$/,/^-----END PUBLIC KEY-----$/p' "$work_dir/cert")" key_pub="$(cat "$work_dir/key.pub")" [ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ] || exit 1 if [ -n "$hostname" ]; then openlist2_tls_run openssl x509 -in "$cert" -noout -ext subjectAltName >"$work_dir/san" 2>/dev/null || exit 1 actual_san="$(sed '1d' "$work_dir/san" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;/^$/d' | LC_ALL=C sort)" expected_san="$( { printf 'DNS:%s\n' "$hostname"; [ -z "$ip" ] || printf 'IP Address:%s\n' "$ip"; } | LC_ALL=C sort )" [ "$actual_san" = "$expected_san" ] || exit 1 fi exit 0 ) openlist2_generate_tls() ( local hostname ip cn san work_dir="" old_cert=0 old_key=0 replacing=0 committed=0 local restore_ok keep_backup=0 status umask 077 openlist2_tls_managed_paths_safe || exit 1 mkdir -p "$tls_dir" && chmod 700 "$tls_dir" || { openlist2_error "Unable to prepare automatic TLS directory: $tls_dir"; exit 1; } hostname="$(openlist2_tls_hostname)" ip="$(openlist2_tls_lan_ip)" if openlist2_tls_pair_valid "$ssl_cert" "$ssl_key" "$OPENLIST2_TLS_RENEW_BEFORE" "$hostname" "$ip"; then chmod 600 "$ssl_key" && chmod 644 "$ssl_cert" exit $? fi work_dir="$(mktemp -d "$tls_dir/.generate.XXXXXX")" || { openlist2_error "Unable to stage automatic TLS certificate files."; exit 1; } # Invoked through the EXIT trap, including interrupted replacements. # shellcheck disable=SC2329 openlist2_tls_cleanup() { status=$? trap - 0 HUP INT TERM if [ "$replacing" = "1" ] && [ "$committed" != "1" ]; then restore_ok=1 if [ "$old_key" = "1" ]; then mv -f "$work_dir/previous.key" "$ssl_key" || restore_ok=0 else rm -f "$ssl_key" || restore_ok=0 fi if [ "$old_cert" = "1" ]; then mv -f "$work_dir/previous.crt" "$ssl_cert" || restore_ok=0 else rm -f "$ssl_cert" || restore_ok=0 fi if [ "$restore_ok" != "1" ]; then keep_backup=1 status=1 openlist2_error "Unable to restore the previous TLS files; recovery files retained at $work_dir" fi fi [ "$keep_backup" = "1" ] || rm -rf "$work_dir" exit "$status" } trap openlist2_tls_cleanup 0 trap 'exit 1' HUP INT TERM cn="${ip:-$hostname}" [ "${#cn}" -le 64 ] || cn=OpenList san="DNS:$hostname" [ -z "$ip" ] || san="$san,IP:$ip" openlist2_tls_run openssl req -x509 -nodes -newkey ec \ -pkeyopt ec_paramgen_curve:prime256v1 -pkeyopt ec_param_enc:named_curve \ -sha256 -days 3650 -keyout "$work_dir/new.key" -out "$work_dir/new.crt" \ -subj "/CN=$cn" -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=digitalSignature" -addext "extendedKeyUsage=serverAuth" \ -addext "subjectAltName=$san" >/dev/null 2>&1 || { openlist2_error "Unable to generate an automatic TLS certificate."; exit 1; } openlist2_tls_pair_valid "$work_dir/new.crt" "$work_dir/new.key" "$OPENLIST2_TLS_RENEW_BEFORE" "$hostname" "$ip" && chmod 600 "$work_dir/new.key" && chmod 644 "$work_dir/new.crt" || { openlist2_error "Unable to validate the generated TLS certificate and key."; exit 1; } openlist2_tls_managed_paths_safe || exit 1 if [ -f "$ssl_cert" ]; then cp -p "$ssl_cert" "$work_dir/previous.crt" || { openlist2_error "Unable to back up the previous TLS certificate."; exit 1; } old_cert=1 fi if [ -f "$ssl_key" ]; then cp -p "$ssl_key" "$work_dir/previous.key" || { openlist2_error "Unable to back up the previous TLS key."; exit 1; } old_key=1 fi replacing=1 mv -f "$work_dir/new.key" "$ssl_key" && mv -f "$work_dir/new.crt" "$ssl_cert" || { openlist2_error "Unable to install the generated TLS certificate pair; restoring previous files."; exit 1; } committed=1 logger -t openlist2 "Generated automatic TLS certificate in $tls_dir" exit 0 ) openlist2_prepare_tls() { [ "$tls_mode" != "off" ] || return 0 command -v openssl >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1 || { openlist2_error "TLS requires openssl-util and coreutils-timeout."; return 1; } if [ "$tls_mode" = "auto" ]; then openlist2_generate_tls else openlist2_tls_pair_valid "$ssl_cert" "$ssl_key" || { openlist2_error "Custom TLS certificate/key is missing, unreadable, invalid, expired or mismatched; custom files were not changed."; return 1; } fi }