#!/usr/bin/env bash
# mugnsoft-front-pki.sh - generate every key and certificate the Webserver (ADMIN)
# and the Webserver-Front (FRONT) need for replication, sorted into one folder per
# host, ready to copy into each install directory.
#
# Requires bash and OpenSSL 1.1.1 or later (Linux, macOS, or Git Bash on Windows).
#
# Usage:
#   ./mugnsoft-front-pki.sh --dns <front-names> --ip <front-ips> [--out <dir>] [--days <n>]
#
#   --dns   Comma-separated DNS names of the FRONT host: the name ADMIN dials
#           (FrontEndpoint) AND the name users type in their browser.
#   --ip    Comma-separated IP addresses of the FRONT host, if ADMIN or users
#           reach it by IP. Optional when --dns is given.
#   --out   Output folder (default: ./mugnsoft-front-pki). Must not exist yet.
#   --days  Validity of the ADMIN client and FRONT server certificates (default 825).
#
# Example (replace the values with YOUR environment's names and addresses):
#   ./mugnsoft-front-pki.sh --dns front.acme.local,status.acme.com --ip 10.20.0.15

set -euo pipefail
# Git Bash: keep "/CN=..." from being rewritten into a Windows path.
export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'

DNS="" IPS="" OUT="./mugnsoft-front-pki" DAYS=825
die() { echo "ERROR: $*" >&2; exit 1; }
usage() { sed -n '2,21p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }

while [ $# -gt 0 ]; do
  case "$1" in
    --dns)  DNS="${2:-}"; shift 2 ;;
    --ip)   IPS="${2:-}"; shift 2 ;;
    --out)  OUT="${2:-}"; shift 2 ;;
    --days) DAYS="${2:-}"; shift 2 ;;
    -h|--help) usage 0 ;;
    *) echo "Unknown option: $1" >&2; usage 1 ;;
  esac
done

command -v openssl >/dev/null || die "openssl not found in PATH"
[ -n "$DNS$IPS" ] || die "give at least one --dns or --ip for the FRONT host (see --help)"
# exact match per entry: the placeholders shown on the doc page and in --help
case ",${DNS// /},${IPS// /}," in
  *,front.acme.local,*|*,status.acme.com,*|*,10.20.0.15,*|*example.com*|*203.0.113.10*) die "the example values must be replaced with your FRONT host's real names/addresses" ;;
esac
[[ "$DAYS" =~ ^[0-9]+$ ]] || die "--days must be a number"
[ -e "$OUT" ] && die "$OUT already exists - choose another --out or move it away (it holds your CA key)"

# Build the subjectAltName list and validate every entry.
SAN="" CN=""
IFS=',' read -ra items <<< "$DNS"
for d in "${items[@]}"; do
  d="${d// /}"; [ -z "$d" ] && continue
  [[ "$d" =~ ^[A-Za-z0-9*]([A-Za-z0-9.-]*[A-Za-z0-9])?$ ]] || die "invalid DNS name: $d"
  SAN="${SAN:+$SAN,}DNS:$d"; CN="${CN:-$d}"
done
IFS=',' read -ra items <<< "$IPS"
for ip in "${items[@]}"; do
  ip="${ip// /}"; [ -z "$ip" ] && continue
  [[ "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ || "$ip" =~ ^[0-9A-Fa-f:]+$ ]] || die "invalid IP address: $ip"
  SAN="${SAN:+$SAN,}IP:$ip"; CN="${CN:-$ip}"
done

umask 077
mkdir -p "$OUT"/{ca,tmp} "$OUT"/admin/pki "$OUT"/front/pki "$OUT"/front/config/ssl/certificates "$OUT"/front/config/ssl/private
cd "$OUT"
q() { local err; err=$("$@" 2>&1 >/dev/null) || { echo "FAILED: $*" >&2; echo "$err" >&2; exit 1; }; }

echo "1/5 Replication CA"
cat > tmp/ca.cnf <<'EOF'
[req]
distinguished_name = dn
x509_extensions = v3_ca
prompt = no
[dn]
CN = mugnsoft-repl-ca
[v3_ca]
basicConstraints = critical,CA:TRUE
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
EOF
q openssl genrsa -out ca/repl_ca.key 4096
q openssl req -x509 -new -key ca/repl_ca.key -sha256 -days 3650 -config tmp/ca.cnf -out ca/repl_ca.crt

echo "2/5 ADMIN client certificate"
cat > tmp/client.ext <<'EOF'
basicConstraints = CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = clientAuth
EOF
q openssl genrsa -out admin/pki/admin_client.key 4096
q openssl req -new -key admin/pki/admin_client.key -subj "/CN=webserver-admin" -out tmp/admin_client.csr
q openssl x509 -req -in tmp/admin_client.csr -CA ca/repl_ca.crt -CAkey ca/repl_ca.key -CAcreateserial \
  -days "$DAYS" -sha256 -extfile tmp/client.ext -out admin/pki/admin_client.crt

echo "3/5 FRONT server certificate ($SAN)"
printf 'basicConstraints = CA:FALSE\nkeyUsage = critical,digitalSignature,keyEncipherment\nextendedKeyUsage = serverAuth\nsubjectAltName = %s\n' "$SAN" > tmp/server.ext
q openssl genrsa -out front/config/ssl/private/webserver.key 4096
q openssl req -new -key front/config/ssl/private/webserver.key -subj "/CN=$CN" -out tmp/front_server.csr
q openssl x509 -req -in tmp/front_server.csr -CA ca/repl_ca.crt -CAkey ca/repl_ca.key -CAcreateserial \
  -days "$DAYS" -sha256 -extfile tmp/server.ext -out front/config/ssl/certificates/webserver.pem

echo "4/5 Token signing keypair and shared payload key"
q openssl genrsa -out admin/pki/repl_sign.key 2048
q openssl rsa -in admin/pki/repl_sign.key -pubout -out front/pki/repl_sign.pub
REPLKEY=$(openssl rand -hex 32)
cp ca/repl_ca.crt admin/pki/repl_ca.crt
cp ca/repl_ca.crt front/pki/repl_ca.crt
rm -rf tmp ca/repl_ca.srl

# Configuration fragments, ReplKey already filled in.
FIRST_HOST="${DNS%%,*}"; FIRST_HOST="${FIRST_HOST// /}"; [ -z "$FIRST_HOST" ] && FIRST_HOST="${IPS%%,*}"
cat > admin/webserver.json.add <<EOF
  "ReplKey": "$REPLKEY",
  "ReplCACert": "pki/repl_ca.crt",
  "ReplClientCert": "pki/admin_client.crt",
  "ReplClientKey": "pki/admin_client.key",
  "ReplSignKey": "pki/repl_sign.key",
  "FrontEnabled": "true",
  "FrontEndpoint": "$FIRST_HOST:8040",
  "FrontReconcileMins": "5",
  "FrontGracePeriodMins": "0"
EOF
cat > front/webserver-front.json <<EOF
{
    "HTTPS": "true",
    "PortAPI": "8040",
    "PortWEB": "9080",
    "RunUser": "",
    "ReplKey": "$REPLKEY",
    "ReplCACert": "pki/repl_ca.crt",
    "ReplPubKey": "pki/repl_sign.pub"
}
EOF

echo "5/5 Self-check"
ok() { echo "  ok  $*"; }
openssl verify -CAfile ca/repl_ca.crt admin/pki/admin_client.crt >/dev/null && ok "ADMIN client cert is signed by the CA"
openssl verify -CAfile ca/repl_ca.crt -purpose sslclient admin/pki/admin_client.crt >/dev/null && ok "ADMIN client cert is valid for client authentication"
openssl verify -CAfile ca/repl_ca.crt -purpose sslserver front/config/ssl/certificates/webserver.pem >/dev/null && ok "FRONT server cert is signed by the CA and valid for server authentication"
[ "$(openssl x509 -in admin/pki/admin_client.crt -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256)" = \
  "$(openssl pkey -in admin/pki/admin_client.key -pubout -outform DER | openssl dgst -sha256)" ] && ok "ADMIN client key matches its certificate"
[ "$(openssl x509 -in front/config/ssl/certificates/webserver.pem -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256)" = \
  "$(openssl pkey -in front/config/ssl/private/webserver.key -pubout -outform DER | openssl dgst -sha256)" ] && ok "FRONT server key matches its certificate"
[ "$(openssl pkey -in admin/pki/repl_sign.key -pubout -outform DER | openssl dgst -sha256)" = \
  "$(openssl pkey -pubin -in front/pki/repl_sign.pub -outform DER | openssl dgst -sha256)" ] && ok "repl_sign.pub is the public half of repl_sign.key"
echo "  SAN of the FRONT certificate:"
openssl x509 -in front/config/ssl/certificates/webserver.pem -noout -ext subjectAltName | sed -n '2p' | sed 's/^ */      /'

cat <<EOF

Done. Output in: $(pwd)

  ca/       repl_ca.key + repl_ca.crt   KEEP OFFLINE - copy to no server
  admin/    copy the CONTENTS into the ADMIN install directory
            then merge admin/webserver.json.add into ADMIN's webserver.json
  front/    copy the CONTENTS into the FRONT install directory
            (webserver-front.json is complete; it replaces the starter one)

Set FrontEndpoint to a name or IP from the SAN list above, with FRONT's PortAPI.
EOF
