#!/usr/bin/env bash
# Local installation lab, not ACME issuance or renewal.
# Requires Linux, Bash, Docker, curl, OpenSSL and GNU coreutils.
# Runs acme.sh 3.1.6 in a temporary home; no cron job or account is installed.
set -euo pipefail
umask 077
for command in docker curl openssl timeout sha256sum stat; do
  command -v "$command" >/dev/null
done
lab_dir=$(mktemp -d)
container=""
cleanup() {
  if [[ -n "$container" ]]; then docker rm -f "$container" >/dev/null 2>&1 || true; fi
  rm -rf "$lab_dir"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
curl --fail --silent --show-error --location \
  https://raw.githubusercontent.com/acmesh-official/acme.sh/807da6498377ee5e0cf43a78091f46f12dc59a89/acme.sh \
  -o "$lab_dir/acme.sh"
printf '%s  %s\n' c7d68b021cfd6380ea83a82962abde5b484779fee0b97d38681dfa1396bbc8d7 \
  "$lab_dir/acme.sh" | sha256sum --check --status
image='nginx@sha256:060849a174666c53439dc7cecbf8f67e225c7cbfb1a44ca97915d9a172a81e6d'
for generation in old new; do
  openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
    -nodes -days 2 -subj '/CN=renewal.test' \
    -addext 'subjectAltName=DNS:renewal.test' \
    -keyout "$lab_dir/$generation.key" -out "$lab_dir/$generation.crt" 2>/dev/null
done
mkdir -p "$lab_dir/acme/renewal.test_ecc" "$lab_dir/live"
# Synthetic internal state ONLY for this version-pinned lab. Do not populate or
# modify a real acme.sh store by hand. No CA issuance is performed here.
cp "$lab_dir/new.key" "$lab_dir/acme/renewal.test_ecc/renewal.test.key"
cp "$lab_dir/new.crt" "$lab_dir/acme/renewal.test_ecc/renewal.test.cer"
cp "$lab_dir/new.crt" "$lab_dir/acme/renewal.test_ecc/fullchain.cer"
printf "Le_Domain='renewal.test'\nLe_Keylength='ec-256'\n" > "$lab_dir/acme/renewal.test_ecc/renewal.test.conf"
cp "$lab_dir/old.key" "$lab_dir/live/key.pem"
cp "$lab_dir/old.crt" "$lab_dir/live/fullchain.pem"
chmod 640 "$lab_dir/live/key.pem"
cat > "$lab_dir/nginx.conf" <<'NGINX'
pid /tmp/nginx.pid;
error_log /dev/stderr notice;
events {}
http {
  client_body_temp_path /tmp/client_temp;
  proxy_temp_path /tmp/proxy_temp;
  fastcgi_temp_path /tmp/fastcgi_temp;
  uwsgi_temp_path /tmp/uwsgi_temp;
  scgi_temp_path /tmp/scgi_temp;
  access_log off;
  server {
    listen 8443 ssl;
    server_name renewal.test;
    ssl_certificate /lab/live/fullchain.pem;
    ssl_certificate_key /lab/live/key.pem;
    location / { return 200 'acme.sh installation lab\n'; }
  }
}
NGINX
container=$(docker run -d --user "$(id -u):$(id -g)" \
  --cap-drop ALL --security-opt no-new-privileges \
  -p 127.0.0.1:0:8443 \
  --mount "type=bind,src=$lab_dir,dst=/lab,readonly" \
  "$image" nginx -c /lab/nginx.conf -g 'daemon off;')
address=$(docker port "$container" 8443/tcp)
file_fingerprint() { openssl x509 -in "$1" -noout -sha256 -fingerprint; }
probe() {
  timeout 5 openssl s_client -connect "$address" -servername renewal.test </dev/null 2>/dev/null |
    openssl x509 -noout -sha256 -fingerprint
}
expect_fingerprint() {
  local expected=$1
  for attempt in {1..40}; do
    if served=$(probe) && [[ "$served" == "$expected" ]]; then return; fi
    sleep 0.25
  done
  echo 'FAIL: served certificate did not match' >&2
  docker logs "$container" >&2
  return 1
}
acme() {
  sh "$lab_dir/acme.sh" --home "$lab_dir/acme" \
    --config-home "$lab_dir/acme" --cert-home "$lab_dir/acme" "$@"
}
install_certificate() {
  acme --install-cert -d renewal.test --ecc \
    --key-file "$lab_dir/live/key.pem" \
    --fullchain-file "$lab_dir/live/fullchain.pem" --reloadcmd "$1"
}
old=$(file_fingerprint "$lab_dir/old.crt")
new=$(file_fingerprint "$lab_dir/new.crt")
[[ "$old" != "$new" ]]
expect_fingerprint "$old"
printf 'BEFORE: %s\n' "$served"
# A failed hook must return an error even though the destination files changed.
if install_certificate false > "$lab_dir/failed-install.log" 2>&1; then
  echo 'FAIL: the deliberately failing reload hook unexpectedly succeeded' >&2
  exit 1
fi
[[ "$(file_fingerprint "$lab_dir/live/fullchain.pem")" == "$new" ]]
expect_fingerprint "$old"
printf 'DISK:   %s\nSERVED: %s\n' "$new" "$served"
reload="docker exec $container nginx -c /lab/nginx.conf -t && docker exec $container nginx -c /lab/nginx.conf -s reload"
install_certificate "$reload"
expect_fingerprint "$new"
[[ "$(stat -c %a "$lab_dir/live/key.pem")" == 640 ]]
acme --info -d renewal.test --ecc > "$lab_dir/info.txt"
grep -Fxq "Le_ReloadCmd=$reload" "$lab_dir/info.txt"
grep -Fxq "Le_RealKeyPath=$lab_dir/live/key.pem" "$lab_dir/info.txt"
grep -Fxq "Le_RealFullChainPath=$lab_dir/live/fullchain.pem" "$lab_dir/info.txt"
printf 'AFTER:  %s\nPASS: failed reload left the old cert served; corrected install changed it and saved the hook.\n' "$served"
