Skip to content

Instantly share code, notes, and snippets.

@maxried
Created March 2, 2026 10:15
Show Gist options
  • Select an option

  • Save maxried/ca32d78ef20c85058817d35a389a9b82 to your computer and use it in GitHub Desktop.

Select an option

Save maxried/ca32d78ef20c85058817d35a389a9b82 to your computer and use it in GitHub Desktop.
Badly tested script, mostly invented by ChatGPT. Tries to created a CSR with a new key that mimic a certificate used by a reachable server. Use this to generate a new CSR. Use it at your own risk, do not pay for certificates unless you understand what you are doing.
#!/usr/bin/env bash
# Create a fresh private key + CSR by copying Subject and SANs from a live TLS certificate
# fetched via `openssl s_client`. The new key matches the *public-key algorithm/parameters*
# of the existing cert (EC curve / RSA bit length / Ed25519 / Ed448).
#
# Half-arse tested ChatGPT produce. Might spawn monsters in nearby. Use at your own risk.
#
# Input formats:
# - hostname
# - hostname:port
# Default port: 443
#
# Optional:
# --starttls <proto> Use STARTTLS mode in s_client
#
# Outputs):
# <cn>.key.pem[.<N>]
# <cn>.csr.pem[.<N>]
#
set -euo pipefail
die() { echo "ERROR: $*" >&2; exit 2; }
openssl_has() {
local subcmd="$1" pattern="$2"
openssl "${subcmd}" -help 2>&1 | grep -qE "${pattern}"
}
list_starttls_values() {
if ! openssl_has s_client ' -starttls'; then
echo " <not supported by this OpenSSL build>"
return 0
fi
local out vals
out="$(openssl s_client -starttls ? 2>&1 || true)"
vals="$(
echo "$out" \
| sed -n '/Value must be one of:/,$p' \
| sed -e '1d' -e 's/^[[:space:]]\+//' \
| grep -E '^[a-z0-9][a-z0-9-]*$' || true
)"
if [[ -n "$vals" ]]; then
echo "$vals" | sed 's/^/ /'
else
echo " <could not parse values; raw output follows>"
echo "$out" | sed 's/^/ /'
fi
}
usage() {
cat <<'EOF'
create a new key + CSR from a live TLS certificate
USAGE
EOF
echo " $0" '[--starttls <proto>] host[:port]'
cat <<'EOF'
OPTIONS
-h, --help
Show this help and exit
--starttls <proto>
Use OpenSSL s_client STARTTLS mode (passed as: -starttls <proto>)
EOF
echo
echo "STARTTLS values supported by your OpenSSL (from: openssl s_client -starttls ?):"
list_starttls_values
}
sanitize_name() {
local s="${1:-}"
s="$(printf '%s' "$s" | tr '[:upper:]' '[:lower:]')"
s="$(printf '%s' "$s" | sed 's/[^a-z0-9._-]/-/g; s/-\{2,\}/-/g; s/^\([-._]\+\)//; s/\([-._]\+\)$//')"
printf '%s' "${s:-}"
}
pick_pair_paths() {
local base="$1"
local i=0 key csr
while :; do
if [[ $i -eq 0 ]]; then
key="${base}.key.pem"
csr="${base}.csr.pem"
else
key="${base}.key.pem.${i}"
csr="${base}.csr.pem.${i}"
fi
if [[ ! -e "$key" && ! -e "$csr" ]]; then
printf '%s\n' "$key" "$csr"
return 0
fi
i=$((i+1))
done
}
atomic_copy_mode() {
local src="$1" dest="$2" mode="$3"
[[ ! -e "$dest" ]] || die "Refusing to overwrite existing file: $dest"
local d bn tmp
d="$(dirname -- "$dest")"
bn="$(basename -- "$dest")"
tmp="$(mktemp -p "$d" ".${bn}.tmp.XXXXXXXX")" || die "mktemp failed in dest dir: $d"
local cleanup_tmp=1
trap '[[ ${cleanup_tmp} -eq 1 ]] && rm -f -- "$tmp"' RETURN
cp -- "$src" "$tmp"
chmod "$mode" "$tmp"
mv -n -- "$tmp" "$dest" || die "Failed to move into place (destination exists?): $dest"
cleanup_tmp=0
trap - RETURN
}
parse_target() {
# Prints: HOST PORT CONNECT SNI_HOST
local t="$1"
local host port connect sni
port="443"
if [[ "$t" =~ ^\[([0-9a-fA-F:]+)\](:([0-9]+))?$ ]]; then
host="${BASH_REMATCH[1]}"
if [[ -n "${BASH_REMATCH[3]:-}" ]]; then
port="${BASH_REMATCH[3]}"
fi
connect="[${host}]:${port}"
sni=""
else
host="$t"
if [[ "$t" == *:* ]]; then
local maybe_port="${t##*:}"
local maybe_host="${t%:*}"
if [[ "$maybe_port" =~ ^[0-9]+$ ]]; then
host="$maybe_host"
port="$maybe_port"
fi
fi
connect="${host}:${port}"
sni="${host}"
fi
[[ -n "$host" ]] || die "Host is empty."
[[ "$port" =~ ^[0-9]+$ ]] || die "Port is not numeric: $port"
printf '%s\n' "$host" "$port" "$connect" "$sni"
}
count_san_entries() {
local s="${1:-}"
[[ -n "$s" ]] || { echo 0; return; }
local n=1 tmp="$s"
while [[ "$tmp" == *","* ]]; do
tmp="${tmp#*,}"
n=$((n+1))
done
echo "$n"
}
detect_key_type_and_param() {
local text; text="$(cat)"
if echo "$text" | grep -qiE 'ED25519'; then
echo "ED25519"; echo ""; return 0
fi
if echo "$text" | grep -qiE 'ED448'; then
echo "ED448"; echo ""; return 0
fi
local ec_oid ec_nist
ec_oid="$(echo "$text" | sed -n 's/^[[:space:]]*ASN1 OID:[[:space:]]*//p' | head -n1)"
ec_nist="$(echo "$text" | sed -n 's/^[[:space:]]*NIST CURVE:[[:space:]]*//p' | head -n1)"
if [[ -n "$ec_oid" || -n "$ec_nist" ]]; then
echo "EC"; echo "${ec_oid:-$ec_nist}"; return 0
fi
# RSA: Modulus+Exponent markers are reliable even when "RSA Public-Key" is absent.
if echo "$text" | grep -qE '^[[:space:]]*Modulus:' && echo "$text" | grep -qE '^[[:space:]]*Exponent:'; then
local bits
bits="$(echo "$text" | sed -n 's/^[[:space:]]*Public-Key: (\([0-9]\+\) bit).*/\1/p' | head -n1)"
echo "RSA"; echo "${bits:-}"; return 0
fi
echo "UNKNOWN"; echo ""; return 0
}
# ---- args ----
STARTTLS_PROTO=""
TARGET=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--starttls) shift; [[ $# -gt 0 ]] || die "--starttls requires an argument"; STARTTLS_PROTO="$1" ;;
--starttls=*) STARTTLS_PROTO="${1#*=}" ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) [[ -z "$TARGET" ]] && TARGET="$1" || die "Unexpected extra argument: $1" ;;
esac
shift
done
if [[ -z "$TARGET" && $# -gt 0 ]]; then TARGET="$1"; fi
[[ -n "$TARGET" ]] || { usage >&2; exit 1; }
readarray -t PARSED < <(parse_target "$TARGET")
HOST="${PARSED[0]}"
PORT="${PARSED[1]}"
CONNECT="${PARSED[2]}"
SNI_HOST="${PARSED[3]}"
# ---- preflight: non-trivial capabilities ----
command -v openssl >/dev/null 2>&1 || die "Missing required command: openssl"
openssl version >/dev/null 2>&1 || die "OpenSSL is not runnable."
openssl_has s_client ' -connect' || die "OpenSSL s_client does not support -connect."
openssl_has s_client ' -showcerts' || die "OpenSSL s_client does not support -showcerts."
openssl_has x509 ' -ext ' || die "OpenSSL x509 does not support -ext (required for SAN extraction)."
openssl_has x509 ' -pubkey' || die "OpenSSL x509 does not support -pubkey."
openssl_has pkey ' -pubin' || die "OpenSSL pkey does not support -pubin."
openssl_has pkey ' -text' || die "OpenSSL pkey does not support -text."
openssl_has req ' -subj' || die "OpenSSL req does not support -subj."
openssl_has req ' -config' || die "OpenSSL req does not support -config."
openssl_has genpkey '' || die "OpenSSL genpkey subcommand not available."
if [[ -n "$SNI_HOST" ]]; then
openssl_has s_client ' -servername' || die "OpenSSL s_client does not support -servername (SNI)."
fi
if [[ -n "$STARTTLS_PROTO" ]]; then
openssl_has s_client ' -starttls' || die "OpenSSL s_client does not support -starttls."
fi
command -v mktemp >/dev/null 2>&1 || die "Missing required command: mktemp"
# ---- temp workspace ----
tmpdir="$(mktemp -d -t csr.XXXXXXXX)" || die "mktemp failed."
cleanup() { rm -rf "${tmpdir}"; }
trap cleanup EXIT
OLD_CERT="${tmpdir}/old-cert.pem"
PUBKEY_PEM="${tmpdir}/pubkey.pem"
CSR_CONF="${tmpdir}/csr.conf"
TMP_KEY="${tmpdir}/new.key.pem"
TMP_CSR="${tmpdir}/new.csr.pem"
# ---- 1) fetch leaf cert ----
SCLIENT_ARGS=( -connect "${CONNECT}" -showcerts )
[[ -n "$SNI_HOST" ]] && SCLIENT_ARGS+=( -servername "${SNI_HOST}" )
[[ -n "$STARTTLS_PROTO" ]] && SCLIENT_ARGS+=( -starttls "${STARTTLS_PROTO}" )
openssl s_client "${SCLIENT_ARGS[@]}" </dev/null \
| openssl x509 -outform PEM > "${OLD_CERT}"
# ---- 2) subject + CN ----
SUBJECT_RFC2253="$(openssl x509 -in "${OLD_CERT}" -noout -subject -nameopt RFC2253 | sed 's/^subject=//')"
SUBJECT_SLASH="/$(echo "${SUBJECT_RFC2253}" | sed 's/,/\//g')"
CN_RAW="$(openssl x509 -in "${OLD_CERT}" -noout -subject -nameopt RFC2253 \
| sed -n 's/^subject=.*CN=\([^,]*\).*$/\1/p' | head -n1)"
CN_RAW="${CN_RAW:-$HOST}"
CN_SAFE="$(sanitize_name "${CN_RAW}")"
[[ -n "${CN_SAFE}" ]] || CN_SAFE="$(sanitize_name "${HOST}")"
[[ -n "${CN_SAFE}" ]] || CN_SAFE="cert"
readarray -t OUTS < <(pick_pair_paths "${CN_SAFE}")
KEY_OUT="${OUTS[0]}"
CSR_OUT="${OUTS[1]}"
# ---- 3) SANs ----
SAN_CSV="$(
openssl x509 -in "${OLD_CERT}" -noout -ext subjectAltName 2>/dev/null \
| sed -n '2,$p' \
| sed 's/^[[:space:]]\+//' \
| tr -d ' \t\r' \
| tr -d '\n' \
| sed -e 's/IP Address:/IP:/g' -e 's/IPAddress:/IP:/g' -e 's/,$//'
)"
SAN_COUNT="$(count_san_entries "${SAN_CSV}")"
# ---- 4) determine public key algorithm/params ----
openssl x509 -in "${OLD_CERT}" -noout -pubkey > "${PUBKEY_PEM}"
PKEY_TEXT="$(openssl pkey -pubin -in "${PUBKEY_PEM}" -text -noout 2>/dev/null)"
readarray -t KP < <(printf '%s' "$PKEY_TEXT" | detect_key_type_and_param)
DETECTED_ALGO="${KP[0]}"
DETECTED_PARAM="${KP[1]}"
[[ "$DETECTED_ALGO" != "UNKNOWN" ]] || die "Unknown/unexpected public key algorithm."
# ---- 5) generate matching key ----
case "$DETECTED_ALGO" in
ED25519) openssl genpkey -algorithm ED25519 -out "${TMP_KEY}" ;;
ED448) openssl genpkey -algorithm ED448 -out "${TMP_KEY}" ;;
EC)
[[ -n "$DETECTED_PARAM" ]] || die "EC detected but curve could not be determined."
openssl genpkey -algorithm EC -pkeyopt "ec_paramgen_curve:${DETECTED_PARAM}" -out "${TMP_KEY}"
;;
RSA)
[[ "$DETECTED_PARAM" =~ ^[0-9]+$ ]] || die "RSA detected but bit length missing/unparseable."
openssl genpkey -algorithm RSA -pkeyopt "rsa_keygen_bits:${DETECTED_PARAM}" -out "${TMP_KEY}"
;;
*) die "Internal error: unsupported algo '$DETECTED_ALGO'" ;;
esac
[[ ! -e "${KEY_OUT}" && ! -e "${CSR_OUT}" ]] || die "Target files already exist: ${KEY_OUT} / ${CSR_OUT}"
atomic_copy_mode "${TMP_KEY}" "${KEY_OUT}" 600
# ---- 6) minimal CSR config ----
cat > "${CSR_CONF}" <<'EOF'
[ req ]
prompt = no
distinguished_name = dn
req_extensions = req_ext
[ dn ]
# Subject is provided via -subj
[ req_ext ]
EOF
[[ -n "${SAN_CSV}" ]] && echo "subjectAltName = ${SAN_CSV}" >> "${CSR_CONF}"
# ---- 7) create CSR ----
openssl req -new -key "${KEY_OUT}" -out "${TMP_CSR}" \
-subj "${SUBJECT_SLASH}" \
-config "${CSR_CONF}"
atomic_copy_mode "${TMP_CSR}" "${CSR_OUT}" 644
# ---- 8) summary ----
algo_pretty="$DETECTED_ALGO"
[[ "$DETECTED_ALGO" == "EC" ]] && algo_pretty="EC (${DETECTED_PARAM})"
[[ "$DETECTED_ALGO" == "RSA" ]] && algo_pretty="RSA (${DETECTED_PARAM} bits)"
echo "=== Detected / Derived ==="
echo "Connected to : ${CONNECT}"
echo "STARTTLS : ${STARTTLS_PROTO:-<none>}"
echo "SNI servername used : ${SNI_HOST:-<none>}"
echo "Certificate subject : ${SUBJECT_RFC2253}"
echo "Public-key algorithm : ${algo_pretty}"
echo "SAN entry count : ${SAN_COUNT}"
[[ -n "${SAN_CSV}" ]] && echo "SAN list : ${SAN_CSV}"
echo
echo "=== Output files ==="
echo "Private key : ${KEY_OUT}"
echo "CSR : ${CSR_OUT}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment