#!/usr/bin/env bash
#
# RELAY-PHASE1 · Native mTLS tunnel · V2 — Device client-CA (and frp-client control-CA).
#
# STAGING TEMPLATE. Mints a PRIVATE client-authentication CA whose leaves the native clients (iOS /
# Android / desktop) present at nginx :8470 (`ssl_verify_client on`). This CA is the ONE AND ONLY
# security gate for the tunnelled base app (the base app has no login of its own — PLAN §1), so it
# MUST be a SEPARATE trust root from Let's Encrypt (the public :8470 SERVER cert) and from the
# agent-enrollment CA (deploy/scripts/gen-agent-ca.sh). Mixing them would let a foreign leaf enroll.
#
#   *** P-256 (prime256v1), NEVER Ed25519. ***  nginx `ssl_verify_client` + iOS client-certificate
#   selection require ECDSA-P256/RSA; Ed25519 client certs are not universally accepted (PLAN V2).
#
# Two KINDs (same structure, different name/subject/default dir):
#     device      → /etc/relay/device-ca      (device-ca.cert.pem)      leaves = client devices (V2/V4)
#     frp-client  → /etc/relay/frp-client-ca  (frp-client-ca.cert.pem)  leaves = per-host frpc control mTLS (V1/V1b)
#
# Produces an OpenSSL `ca` database (index.txt / serial / crlnumber / newcerts / openssl.cnf) so that
# deploy/scripts/issue-device-cert.sh can sign tracked leaves and deploy/scripts/revoke-device.sh can
# revoke by serial and regenerate the CRL. An EMPTY crl.pem is initialised NOW so V4's `ssl_crl` has a
# file to reference from day one.
#
# Outputs (0600 key, 0644 cert) under CA_DIR:
#     <CA_NAME>.key.pem   <CA_NAME>.cert.pem   crl.pem
#     index.txt  serial  crlnumber  index.txt.attr  newcerts/  openssl.cnf   (the `openssl ca` db)
#
# Config via ENV or flags (no hardcoded secrets — this CA key is generated in place, never committed):
#     KIND        device | frp-client            (default device)          — or  --kind <k>
#     CA_DIR      output dir                      (default per-KIND above)  — or  --dir <path>
#     CA_DAYS     CA lifetime in days             (default 3650)
#
# Idempotent: an existing CA (key+cert) is REUSED, never overwritten (delete the dir to rotate).
#
# Verify (syntax): bash -n deploy/scripts/gen-device-ca.sh
set -euo pipefail

KIND="${KIND:-device}"
CA_DIR="${CA_DIR:-}"
CA_DAYS="${CA_DAYS:-3650}"

# ---- args (flags override env) --------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
  case "$1" in
    --kind) KIND="${2:?--kind needs a value}"; shift 2 ;;
    --dir)  CA_DIR="${2:?--dir needs a value}"; shift 2 ;;
    -h|--help)
      grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
    *) echo "FATAL: unknown argument '$1' (see --help)" >&2; exit 2 ;;
  esac
done

case "${KIND}" in
  device)     CA_NAME="device-ca";     CA_CN="web-terminal Device CA";     CA_OU="device-ca"
              CA_DIR="${CA_DIR:-/etc/relay/device-ca}" ;;
  frp-client) CA_NAME="frp-client-ca"; CA_CN="web-terminal frp-client CA"; CA_OU="frp-client-ca"
              CA_DIR="${CA_DIR:-/etc/relay/frp-client-ca}" ;;
  *) echo "FATAL: KIND must be 'device' or 'frp-client' (got '${KIND}')." >&2; exit 2 ;;
esac

command -v openssl >/dev/null 2>&1 || { echo "FATAL: openssl not found on PATH." >&2; exit 3; }

umask 077
mkdir -p "${CA_DIR}/newcerts"
# Resolve to an absolute path so `openssl ca` (which we run cd'd into CA_DIR with dir=.) is stable.
CA_DIR="$(cd "${CA_DIR}" && pwd)"

KEY="${CA_DIR}/${CA_NAME}.key.pem"
CERT="${CA_DIR}/${CA_NAME}.cert.pem"

echo "== Device/frp-client CA (${KIND}, PRIVATE — not Let's Encrypt) into ${CA_DIR} =="

# ---- 1. CA keypair + self-signed cert (P-256, CA:TRUE, keyCertSign+cRLSign) -----------------------
if [[ -f "${KEY}" && -f "${CERT}" ]]; then
  echo ">> CA already exists — reusing (delete ${CA_DIR} to rotate)."
else
  openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "${KEY}"
  openssl req -x509 -new -key "${KEY}" -days "${CA_DAYS}" \
    -subj "/O=web-terminal-relay/OU=${CA_OU}/CN=${CA_CN}" \
    -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
    -addext "keyUsage=critical,keyCertSign,cRLSign" \
    -addext "subjectKeyIdentifier=hash" \
    -out "${CERT}"
fi

# ---- 2. OpenSSL `ca` database (so leaves are tracked → revocable via the CRL) ----------------------
[[ -f "${CA_DIR}/index.txt"   ]] || : > "${CA_DIR}/index.txt"
[[ -f "${CA_DIR}/serial"      ]] || echo 1000 > "${CA_DIR}/serial"
[[ -f "${CA_DIR}/crlnumber"   ]] || echo 1000 > "${CA_DIR}/crlnumber"
# unique_subject=no allows re-issuing a leaf for the same CN (device/host cert rotation).
[[ -f "${CA_DIR}/index.txt.attr" ]] || echo "unique_subject = no" > "${CA_DIR}/index.txt.attr"

if [[ ! -f "${CA_DIR}/openssl.cnf" ]]; then
  # dir = .  → the issue/revoke scripts `cd` into CA_DIR before invoking `openssl ca`, so the db is
  # relocatable (works in a mktemp test dir). $dir is expanded by openssl; ${CA_NAME} by bash here.
  cat > "${CA_DIR}/openssl.cnf" <<EOF
[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = .
database          = \$dir/index.txt
serial            = \$dir/serial
new_certs_dir     = \$dir/newcerts
certificate       = \$dir/${CA_NAME}.cert.pem
private_key       = \$dir/${CA_NAME}.key.pem
crlnumber         = \$dir/crlnumber
crl               = \$dir/crl.pem
default_md        = sha256
default_days      = 825
default_crl_days  = 30
policy            = policy_anything
copy_extensions   = none
x509_extensions   = client_cert
crl_extensions    = crl_ext

[ policy_anything ]
countryName            = optional
stateOrProvinceName    = optional
localityName           = optional
organizationName       = optional
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

# Leaf profile: client-authentication ONLY (never serverAuth, never CA).
[ client_cert ]
basicConstraints       = critical, CA:FALSE
keyUsage               = critical, digitalSignature
extendedKeyUsage       = clientAuth
subjectKeyIdentifier    = hash
authorityKeyIdentifier = keyid,issuer

[ crl_ext ]
authorityKeyIdentifier = keyid:always
EOF
fi

# ---- 3. Empty CRL now (V4 `ssl_crl` needs the file to exist even with zero revocations) ------------
if [[ ! -f "${CA_DIR}/crl.pem" ]]; then
  ( cd "${CA_DIR}" && openssl ca -config openssl.cnf -gencrl -out crl.pem >/dev/null 2>&1 )
fi

# ---- 4. Permissions + summary ---------------------------------------------------------------------
chmod 700 "${CA_DIR}"
chmod 600 "${KEY}"
chmod 644 "${CERT}" "${CA_DIR}/crl.pem"

cat <<SUMMARY

${KIND} CA ready in ${CA_DIR}:
  CA cert   = ${CERT}          (0644 — the trust anchor to deploy)
  CA key    = ${KEY}   (0600 — NEVER commit / NEVER leave a shared host)
  CRL       = ${CA_DIR}/crl.pem                (empty; regenerated by revoke-device.sh)
  ca db     = index.txt / serial / crlnumber / newcerts / openssl.cnf

Wire it:
  device      → nginx :8470  ssl_client_certificate ${CERT} ;  ssl_crl ${CA_DIR}/crl.pem ;
  frp-client  → frps.toml    transport.tls.trustedCaFile = ${CERT}

Next:
  issue a leaf   :  CA_DIR=${CA_DIR} bash deploy/scripts/issue-device-cert.sh <CN> <out-dir>
  revoke a leaf  :  CA_DIR=${CA_DIR} bash deploy/scripts/revoke-device.sh <leaf.cert.pem>
SUMMARY
