Files
deamonkai fc94008530 initial
2026-01-23 12:11:21 -06:00

204 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Generate an AES-encrypted RSA or EC private key in PEM format."""
from __future__ import annotations
import argparse
import getpass
import os
import shutil
import subprocess
from pathlib import Path
from typing import Optional
from certctl.console import NitroConsoleClient
from certctl.secretstore import get_secret, is_available, set_secret
DEFAULT_RSA_BITS = 4096
DEFAULT_EC_CURVE = "secp384r1"
def _require_openssl() -> str:
path = shutil.which("openssl")
if not path:
raise SystemExit("OpenSSL not found in PATH.")
return path
def _timestamp() -> str:
import datetime as dt
return dt.datetime.now().strftime("%Y%m%d-%H%M%S")
def _build_key_name(cn: str, stamp: str) -> str:
return f"{cn}-{stamp}.key"
def _get_passphrase(args: argparse.Namespace) -> str:
if args.passphrase:
return args.passphrase
env = os.environ.get("CERTCTL_KEY_PASSPHRASE")
if env:
return env
if args.keychain_service and is_available():
stored = get_secret(args.keychain_service, args.keychain_username)
if stored:
return stored
while True:
pw = getpass.getpass("Key passphrase (AES-256): ")
confirm = getpass.getpass("Confirm passphrase: ")
if pw and pw == confirm:
if args.keychain_service and args.save_passphrase:
set_secret(args.keychain_service, args.keychain_username, pw)
return pw
print("Passphrases did not match, try again.")
def _run(cmd: list[str], env: Optional[dict[str, str]] = None) -> None:
subprocess.run(cmd, check=True, env=env)
def _generate_key(kind: str, out_path: Path, passphrase: str) -> None:
_require_openssl()
if kind == "rsa":
cmd = [
"openssl",
"genpkey",
"-algorithm",
"RSA",
"-pkeyopt",
f"rsa_keygen_bits:{DEFAULT_RSA_BITS}",
"-aes-256-cbc",
"-pass",
f"pass:{passphrase}",
"-out",
str(out_path),
]
else:
cmd = [
"openssl",
"genpkey",
"-algorithm",
"EC",
"-pkeyopt",
f"ec_paramgen_curve:{DEFAULT_EC_CURVE}",
"-pkeyopt",
"ec_param_enc:named_curve",
"-aes-256-cbc",
"-pass",
f"pass:{passphrase}",
"-out",
str(out_path),
]
_run(cmd)
os.chmod(out_path, 0o600)
def _upload_to_console(args: argparse.Namespace, key_path: Path, passphrase: str) -> None:
if not args.upload_console:
return
verify: object
if args.insecure:
verify = False
elif args.ca_bundle:
verify = args.ca_bundle
else:
verify = True
password = args.console_password or os.environ.get("CERTCTL_CONSOLE_PASSWORD")
if not password:
password = getpass.getpass(f"Console password for {args.user}@{args.console}: ")
client = NitroConsoleClient(base=args.console, verify=verify, timeout=args.timeout)
client.login(args.user, password)
client.upload_file(
"/nitro/v2/upload/ns_ssl_key",
str(key_path),
basic_user=args.user,
basic_password=password,
)
if args.register_console:
algo = "RSA" if args.kind == "rsa" else "ECDSA"
keysize = DEFAULT_RSA_BITS if args.kind == "rsa" else None
ec_curve = DEFAULT_EC_CURVE if args.kind == "ecdsa" else None
client.create_key(
key_path.name,
algo=algo,
keyform="PEM",
keysize=keysize,
ec_curve=ec_curve,
password=passphrase,
)
def run(args: argparse.Namespace) -> int:
passphrase = _get_passphrase(args)
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
stamp = args.stamp or _timestamp()
key_name = _build_key_name(args.cn, stamp)
key_path = out_dir / key_name
_generate_key(args.kind, key_path, passphrase)
print(f"Wrote key: {key_path}")
if args.upload_console:
if not args.console or not args.user:
raise SystemExit("Console URL and user are required for --upload-console.")
_upload_to_console(args, key_path, passphrase)
print("Uploaded key to NetScaler Console.")
if args.register_console:
print("Registered key metadata in NetScaler Console.")
return 0
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Generate an AES-encrypted RSA or EC private key.")
parser.add_argument("--cn", required=True, help="Common Name (CN) for the key filename")
parser.add_argument("--kind", choices=["rsa", "ecdsa"], required=True, help="Key type")
parser.add_argument("--out", default="./out", help="Output directory for key files")
parser.add_argument("--stamp", help="Timestamp to use in the filename (e.g., 20260101-120000)")
parser.add_argument("--passphrase", help="Key passphrase (or set CERTCTL_KEY_PASSPHRASE)")
parser.add_argument("--keychain-service", help="Keychain service name for passphrase storage")
parser.add_argument(
"--keychain-username",
default=getpass.getuser(),
help="Keychain username (default: current user)",
)
parser.add_argument(
"--save-passphrase",
action="store_true",
default=False,
help="Store passphrase in keychain (requires keyring)",
)
parser.add_argument(
"--upload-console",
action="store_true",
default=False,
help="Upload the key to NetScaler Console",
)
parser.add_argument(
"--register-console",
action="store_true",
default=False,
help="Register key metadata in NetScaler Console after upload",
)
parser.add_argument("--console", help="Console base URL, e.g. https://console")
parser.add_argument("--user", help="Console username")
parser.add_argument("--console-password", help="Console password (or set CERTCTL_CONSOLE_PASSWORD)")
parser.add_argument("--insecure", action="store_true", help="Disable TLS verification")
parser.add_argument("--ca-bundle", help="Path to CA bundle for TLS verification")
parser.add_argument("--timeout", type=int, default=60, help="HTTP timeout in seconds")
return parser
def main() -> None:
parser = build_arg_parser()
args = parser.parse_args()
raise SystemExit(run(args))
if __name__ == "__main__":
main()