358 lines
10 KiB
Shell
Executable file
358 lines
10 KiB
Shell
Executable file
#!/usr/bin/env bash
|
|
# lab-test-ca.sh — OpenSSL lab CA for local/dev experiments ONLY.
|
|
#
|
|
# Creates a self-signed lab root CA and signs a PKCS#10 CSR → leaf PEM.
|
|
# Material stays on disk under a lab directory (default lab-pki/, gitignored).
|
|
#
|
|
# Optional HTTP path (LAB ONLY): when staff-iam is started with LAB_CA_ENABLE=1
|
|
# AND this directory has been initialized, POST /enroll/csr may shell-out to
|
|
# `sign` and return 201 + cert PEM with header X-Lab-CA: 1.
|
|
# Default (LAB_CA_ENABLE unset/off) → POST /enroll/csr stays 501.
|
|
# NODE_ENV=production always refuses the lab HTTP path.
|
|
#
|
|
# NOT FOR PRODUCTION. Do not trust this root on real workstations,
|
|
# staging staff fleets, or prod reverse proxies.
|
|
#
|
|
# Usage:
|
|
# scripts/lab-test-ca.sh init [--dir DIR]
|
|
# scripts/lab-test-ca.sh sign --csr PATH [--dir DIR] [--out PATH] [--days N] [--cn CN]
|
|
# scripts/lab-test-ca.sh crl [--dir DIR] [--out PATH]
|
|
# scripts/lab-test-ca.sh warn
|
|
#
|
|
# Defaults: DIR=./lab-pki (gitignored). Requires openssl on PATH.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
DEFAULT_DIR="${ROOT_DIR}/lab-pki"
|
|
DAYS_DEFAULT=7
|
|
|
|
warn_banner() {
|
|
cat >&2 <<'EOF'
|
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
LAB ONLY — verae-staff-iam scripts/lab-test-ca.sh
|
|
Self-signed throwaway CA. NOT for production, staging staff, or
|
|
any reverse-proxy trust store that faces real operators.
|
|
Opt-in HTTP: LAB_CA_ENABLE=1 + init'd lab-pki → POST /enroll/csr may
|
|
shell-out here (201 + X-Lab-CA:1). Default off → 501 scaffold.
|
|
NODE_ENV=production refuses lab HTTP. NOT a production CA.
|
|
Do NOT ship lab-pki/ keys in images or commit private keys.
|
|
Live production mint stays on the product backlog (docs/PKI-MINT-BACKLOG.md).
|
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
EOF
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage:
|
|
$SCRIPT_NAME init [--dir DIR]
|
|
Create lab root CA key + self-signed cert under DIR (default: lab-pki/).
|
|
|
|
$SCRIPT_NAME sign --csr PATH [--dir DIR] [--out PATH] [--days N] [--cn CN]
|
|
Sign a PEM CSR with the lab CA → leaf certificate PEM.
|
|
--out defaults to DIR/leaf-<timestamp>.crt.pem
|
|
--days leaf notAfter (default ${DAYS_DEFAULT}, max 30 for lab)
|
|
--cn optional subject CN override in the issued cert (lab only)
|
|
|
|
$SCRIPT_NAME crl [--dir DIR] [--out PATH]
|
|
Write a lab-only **empty** CRL (no revoked serials) signed by the lab root.
|
|
--out defaults to DIR/lab-empty.crl.pem
|
|
For local proxy CRL-path smoke tests only — NOT production revocation.
|
|
|
|
$SCRIPT_NAME warn
|
|
Print the lab-only warning and exit 0.
|
|
|
|
Environment:
|
|
LAB_PKI_DIR Override default DIR when --dir omitted.
|
|
|
|
Exit codes: 0 ok · 1 usage/input · 2 missing openssl/CA · 3 sign/CRL failure
|
|
EOF
|
|
}
|
|
|
|
need_openssl() {
|
|
if ! command -v openssl >/dev/null 2>&1; then
|
|
echo "error: openssl not found on PATH" >&2
|
|
exit 2
|
|
fi
|
|
}
|
|
|
|
resolve_dir() {
|
|
local d="${1:-}"
|
|
if [[ -z "$d" ]]; then
|
|
d="${LAB_PKI_DIR:-$DEFAULT_DIR}"
|
|
fi
|
|
# Absolute path
|
|
if [[ "$d" != /* ]]; then
|
|
d="$(pwd)/$d"
|
|
fi
|
|
echo "$d"
|
|
}
|
|
|
|
cmd_init() {
|
|
local dir=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dir) dir="${2:-}"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "error: unknown init arg: $1" >&2; usage >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
need_openssl
|
|
warn_banner
|
|
dir="$(resolve_dir "$dir")"
|
|
mkdir -p "$dir"
|
|
chmod 700 "$dir" 2>/dev/null || true
|
|
|
|
local key="$dir/lab-root.key.pem"
|
|
local crt="$dir/lab-root.crt.pem"
|
|
local conf="$dir/lab-root.cnf"
|
|
local serial="$dir/lab-root.srl"
|
|
|
|
if [[ -f "$key" || -f "$crt" ]]; then
|
|
echo "error: lab CA already exists under $dir (refusing to overwrite)" >&2
|
|
echo " remove lab-root.key.pem / lab-root.crt.pem to re-init" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cat >"$conf" <<'EOF'
|
|
[req]
|
|
distinguished_name = dn
|
|
x509_extensions = v3_ca
|
|
prompt = no
|
|
|
|
[dn]
|
|
CN = Verae staff-iam LAB root (NOT FOR PRODUCTION)
|
|
O = Verae Lab Only
|
|
OU = throwaway-test-ca
|
|
|
|
[v3_ca]
|
|
basicConstraints = critical,CA:true,pathlen:0
|
|
keyUsage = critical,keyCertSign,cRLSign
|
|
subjectKeyIdentifier = hash
|
|
EOF
|
|
|
|
openssl genrsa -out "$key" 4096 2>/dev/null
|
|
chmod 600 "$key"
|
|
openssl req -new -x509 -days 3650 -key "$key" -out "$crt" -config "$conf"
|
|
# Fresh serial file for leaf issuance
|
|
openssl rand -hex 8 | tr '[:lower:]' '[:upper:]' >"$serial"
|
|
chmod 600 "$serial" 2>/dev/null || true
|
|
|
|
cat >"$dir/README.txt" <<EOF
|
|
LAB PKI material — NOT FOR PRODUCTION
|
|
Generated by scripts/lab-test-ca.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
Root cert: lab-root.crt.pem (public)
|
|
Root key: lab-root.key.pem (PRIVATE — never commit, never production trust)
|
|
With LAB_CA_ENABLE=1 the staff-iam server may shell-out sign against this dir.
|
|
Default (flag off) POST /enroll/csr stays 501. NEVER production.
|
|
See docs/TEST-CA-DESIGN.md, docs/CRL-OCSP-DESIGN.md, ENROLL.md, docs/PKI-MINT-BACKLOG.md.
|
|
EOF
|
|
|
|
echo "lab CA created:"
|
|
echo " dir: $dir"
|
|
echo " cert: $crt"
|
|
echo " key: $key (keep private)"
|
|
openssl x509 -in "$crt" -noout -subject -dates 2>/dev/null || true
|
|
warn_banner
|
|
}
|
|
|
|
cmd_sign() {
|
|
local dir="" csr="" out="" days="$DAYS_DEFAULT" cn=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dir) dir="${2:-}"; shift 2 ;;
|
|
--csr) csr="${2:-}"; shift 2 ;;
|
|
--out) out="${2:-}"; shift 2 ;;
|
|
--days) days="${2:-}"; shift 2 ;;
|
|
--cn) cn="${2:-}"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "error: unknown sign arg: $1" >&2; usage >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$csr" ]]; then
|
|
echo "error: --csr PATH is required" >&2
|
|
usage >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "$csr" ]]; then
|
|
echo "error: CSR not found: $csr" >&2
|
|
exit 1
|
|
fi
|
|
if ! [[ "$days" =~ ^[0-9]+$ ]] || [[ "$days" -lt 1 || "$days" -gt 30 ]]; then
|
|
echo "error: --days must be 1..30 (lab short-lived leaves)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
need_openssl
|
|
warn_banner
|
|
dir="$(resolve_dir "$dir")"
|
|
local key="$dir/lab-root.key.pem"
|
|
local ca="$dir/lab-root.crt.pem"
|
|
local serial="$dir/lab-root.srl"
|
|
local ext="$dir/lab-leaf-ext.cnf"
|
|
|
|
if [[ ! -f "$key" || ! -f "$ca" ]]; then
|
|
echo "error: lab CA missing under $dir — run: $SCRIPT_NAME init --dir $dir" >&2
|
|
exit 2
|
|
fi
|
|
if [[ ! -f "$serial" ]]; then
|
|
openssl rand -hex 8 | tr '[:lower:]' '[:upper:]' >"$serial"
|
|
fi
|
|
|
|
# Light shape check (headers only); openssl does the real parse on sign.
|
|
if ! grep -q "BEGIN CERTIFICATE REQUEST" "$csr" || ! grep -q "END CERTIFICATE REQUEST" "$csr"; then
|
|
echo "error: CSR must be PEM PKCS#10 (BEGIN/END CERTIFICATE REQUEST)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$out" ]]; then
|
|
out="$dir/leaf-$(date -u +%Y%m%dT%H%M%SZ).crt.pem"
|
|
fi
|
|
|
|
cat >"$ext" <<EOF
|
|
basicConstraints = critical,CA:false
|
|
keyUsage = critical,digitalSignature,keyEncipherment
|
|
extendedKeyUsage = clientAuth
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid,issuer
|
|
EOF
|
|
# Optional CN override via -subj would replace whole subject; keep CSR subject
|
|
# unless --cn set, then use openssl -subj for lab convenience only.
|
|
local subj_args=()
|
|
if [[ -n "$cn" ]]; then
|
|
subj_args=(-subj "/CN=${cn}/O=Verae Lab Only/OU=lab-leaf")
|
|
fi
|
|
|
|
if ! openssl x509 -req \
|
|
-in "$csr" \
|
|
-CA "$ca" \
|
|
-CAkey "$key" \
|
|
-CAserial "$serial" \
|
|
-out "$out" \
|
|
-days "$days" \
|
|
-sha256 \
|
|
-extfile "$ext" \
|
|
"${subj_args[@]+"${subj_args[@]}"}" 2>"$dir/last-sign.err"
|
|
then
|
|
echo "error: openssl sign failed:" >&2
|
|
cat "$dir/last-sign.err" >&2 || true
|
|
exit 3
|
|
fi
|
|
rm -f "$dir/last-sign.err"
|
|
|
|
# Chain file (leaf + lab root) for convenience — still lab only.
|
|
local chain="${out%.crt.pem}.chain.pem"
|
|
if [[ "$chain" == "$out" ]]; then
|
|
chain="${out}.chain.pem"
|
|
fi
|
|
cat "$out" "$ca" >"$chain"
|
|
|
|
echo "lab leaf signed (NOT FOR PRODUCTION):"
|
|
echo " leaf: $out"
|
|
echo " chain: $chain"
|
|
openssl x509 -in "$out" -noout -subject -issuer -dates -serial 2>/dev/null || true
|
|
echo
|
|
echo "Next (lab/dev only): point a *dev* reverse proxy trust store at:"
|
|
echo " $ca"
|
|
echo "Default POST /enroll/csr is still 501 unless LAB_CA_ENABLE=1."
|
|
echo "See docs/TEST-CA-DESIGN.md — LAB ONLY, never production."
|
|
warn_banner
|
|
}
|
|
|
|
cmd_crl() {
|
|
local dir="" out=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dir) dir="${2:-}"; shift 2 ;;
|
|
--out) out="${2:-}"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "error: unknown crl arg: $1" >&2; usage >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
need_openssl
|
|
warn_banner
|
|
dir="$(resolve_dir "$dir")"
|
|
local key="$dir/lab-root.key.pem"
|
|
local ca="$dir/lab-root.crt.pem"
|
|
|
|
if [[ ! -f "$key" || ! -f "$ca" ]]; then
|
|
echo "error: lab CA missing under $dir — run: $SCRIPT_NAME init --dir $dir" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [[ -z "$out" ]]; then
|
|
out="$dir/lab-empty.crl.pem"
|
|
fi
|
|
|
|
# Empty CRL: no revoked serials. Lab smoke-test fixture only.
|
|
local crl_cnf="$dir/lab-crl.cnf"
|
|
local crl_idx="$dir/lab-crl-index.txt"
|
|
local crl_attr="$dir/lab-crl-index.txt.attr"
|
|
: >"$crl_idx"
|
|
echo "unique_subject = no" >"$crl_attr"
|
|
cat >"$crl_cnf" <<EOF
|
|
[ ca ]
|
|
default_ca = lab_ca
|
|
|
|
[ lab_ca ]
|
|
dir = $dir
|
|
database = $crl_idx
|
|
serial = $dir/lab-root.srl
|
|
certificate = $ca
|
|
private_key = $key
|
|
default_md = sha256
|
|
default_days = 30
|
|
default_crl_days = 30
|
|
crlnumber = $dir/lab-crlnumber
|
|
copy_extensions = none
|
|
policy = policy_any
|
|
|
|
[ policy_any ]
|
|
commonName = optional
|
|
EOF
|
|
if [[ ! -f "$dir/lab-crlnumber" ]]; then
|
|
echo "01" >"$dir/lab-crlnumber"
|
|
fi
|
|
|
|
if ! openssl ca -gencrl -config "$crl_cnf" -out "$out" 2>"$dir/last-crl.err"; then
|
|
echo "error: openssl gencrl failed:" >&2
|
|
cat "$dir/last-crl.err" >&2 || true
|
|
exit 3
|
|
fi
|
|
rm -f "$dir/last-crl.err"
|
|
|
|
echo "lab empty CRL written (NOT FOR PRODUCTION):"
|
|
echo " crl: $out"
|
|
echo " revoked entries: 0 (empty)"
|
|
openssl crl -in "$out" -noout -text 2>/dev/null | head -n 20 || true
|
|
echo
|
|
echo "Dev proxy only: point CRL path at this file if testing verify+CRL config."
|
|
echo "See docs/CRL-OCSP-DESIGN.md — empty lab CRL, never production revocation."
|
|
warn_banner
|
|
}
|
|
|
|
main() {
|
|
local cmd="${1:-}"
|
|
if [[ -z "$cmd" ]]; then
|
|
warn_banner
|
|
usage >&2
|
|
exit 1
|
|
fi
|
|
shift || true
|
|
case "$cmd" in
|
|
init) cmd_init "$@" ;;
|
|
sign) cmd_sign "$@" ;;
|
|
crl) cmd_crl "$@" ;;
|
|
warn) warn_banner; exit 0 ;;
|
|
-h|--help|help) usage; exit 0 ;;
|
|
*)
|
|
echo "error: unknown command: $cmd" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|