Files
nscertkeycreate/certctl/scripts/csr_create.py
deamonkai fc94008530 initial
2026-01-23 12:11:21 -06:00

218 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""Create a CSR from an encrypted private key with optional SANs."""
from __future__ import annotations
import argparse
import getpass
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Iterable, Optional
def _require_openssl() -> str:
path = shutil.which("openssl")
if not path:
raise SystemExit("OpenSSL not found in PATH.")
return path
def _normalize_sans(values: Iterable[str]) -> list[str]:
normalized = []
for value in values:
parts = [part.strip() for part in value.split(",") if part.strip()]
normalized.extend(parts)
return normalized
def _write_openssl_config(subject: str, sans: Iterable[str]) -> str:
alt_lines = []
dns_i = 1
ip_i = 1
for san in _normalize_sans(sans):
if san.lower().startswith("dns:"):
alt_lines.append(f"DNS.{dns_i} = {san[4:]}")
dns_i += 1
elif san.lower().startswith("ip:"):
alt_lines.append(f"IP.{ip_i} = {san[3:]}")
ip_i += 1
elif all(part.isdigit() for part in san.split(".")) and san.count(".") == 3:
alt_lines.append(f"IP.{ip_i} = {san}")
ip_i += 1
else:
alt_lines.append(f"DNS.{dns_i} = {san}")
dns_i += 1
alt_block = "\n".join(alt_lines) if alt_lines else ""
if alt_lines:
cfg = f"""[req]
default_bits = 2048
prompt = no
distinguished_name = dn
req_extensions = v3_req
[dn]
{_subject_to_dn(subject)}
[v3_req]
subjectAltName = @alt_names
[alt_names]
{alt_block}
"""
else:
cfg = f"""[req]
default_bits = 2048
prompt = no
distinguished_name = dn
[dn]
{_subject_to_dn(subject)}
"""
handle = tempfile.NamedTemporaryFile("w", delete=False)
handle.write(cfg)
handle.close()
return handle.name
def _timestamp() -> str:
import datetime as dt
return dt.datetime.now().strftime("%Y%m%d-%H%M%S")
def _subject_to_dn(subject: str) -> str:
subject = subject.strip()
if subject.startswith("/"):
parts = [p for p in subject.split("/") if p]
kv_pairs = [p.split("=", 1) for p in parts if "=" in p]
else:
kv_pairs = [p.split("=", 1) for p in subject.split(",") if "=" in p]
lines = []
for key, value in kv_pairs:
lines.append(f"{key.strip()} = {value.strip()}")
return "\n".join(lines)
def _extract_cn(subject: str) -> str:
subject = subject.strip()
if subject.startswith("/"):
parts = [p for p in subject.split("/") if p]
for part in parts:
if part.upper().startswith("CN="):
return part.split("=", 1)[1].strip()
return ""
for part in subject.split(","):
if part.strip().upper().startswith("CN="):
return part.split("=", 1)[1].strip()
return ""
def _build_subject_from_args(args: argparse.Namespace) -> str:
if args.subject:
return args.subject
cn = args.cn
if not cn:
cn = input("Common Name (CN): ").strip()
if not cn:
raise SystemExit("Common Name (CN) is required.")
parts = [
f"/C={args.country}",
f"/ST={args.state}",
f"/L={args.locality}",
f"/O={args.organization}",
f"/OU={args.org_unit}",
f"/CN={cn}",
f"/emailAddress={args.email}",
]
return "".join(parts)
def _run(cmd: list[str], env: Optional[dict[str, str]] = None) -> None:
subprocess.run(cmd, check=True, env=env)
def _get_passphrase(args: argparse.Namespace) -> str:
if args.passphrase:
return args.passphrase
env = os.environ.get("CERTCTL_KEY_PASSPHRASE")
if env:
return env
return getpass.getpass("Key passphrase (AES-256): ")
def _build_csr_name(cn: str, stamp: str) -> str:
return f"{cn}-{stamp}.csr"
def run(args: argparse.Namespace) -> int:
_require_openssl()
passphrase = _get_passphrase(args)
key_path = Path(args.key_file)
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
subject = _build_subject_from_args(args)
cn = args.cn or _extract_cn(subject)
if not cn:
raise SystemExit("Common Name (CN) is required to build the CSR filename.")
stamp = args.stamp or _timestamp()
out_path = out_dir / _build_csr_name(cn, stamp)
cfg_path = _write_openssl_config(subject, args.san or [])
try:
cmd = [
"openssl",
"req",
"-new",
"-key",
str(key_path),
"-out",
str(out_path),
"-config",
cfg_path,
"-passin",
f"pass:{passphrase}",
]
_run(cmd)
finally:
os.unlink(cfg_path)
print(f"Wrote CSR: {out_path}")
return 0
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Create a CSR from an encrypted private key.")
parser.add_argument("--key-file", required=True, help="Path to encrypted private key PEM")
parser.add_argument("--subject", help="Subject (e.g., /C=US/ST=CA/CN=example.com)")
parser.add_argument("--cn", help="Common Name (CN) when building a subject from defaults")
parser.add_argument("--country", default="US", help="CountryName (C)")
parser.add_argument("--state", default="Alabama", help="StateName (ST)")
parser.add_argument("--organization", default="Regions Financial Corporation", help="OrganizationName (O)")
parser.add_argument("--org-unit", default="ECommerce", help="OrganizationITName (OU)")
parser.add_argument("--locality", default="Birmingham", help="LocalityName (L)")
parser.add_argument("--email", default="was@regions.com", help="emailAddress")
parser.add_argument(
"--san",
action="append",
help="SubjectAltName entries (comma-separated or repeatable)",
)
parser.add_argument("--out", default="./out", help="Output directory for CSR files")
parser.add_argument("--passphrase", help="Key passphrase (or set CERTCTL_KEY_PASSPHRASE)")
parser.add_argument("--stamp", help="Timestamp to use in the filename (e.g., 20260101-120000)")
return parser
def main() -> None:
parser = build_arg_parser()
args = parser.parse_args()
raise SystemExit(run(args))
if __name__ == "__main__":
main()