204 lines
6.4 KiB
Python
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()
|