This commit is contained in:
deamonkai
2026-01-23 12:11:21 -06:00
commit fc94008530
16494 changed files with 2974672 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
"""nscert package
Lightweight package scaffold for Netscaler certificate tooling.
"""
__all__ = ["utils", "netscaler"]

13
legacy/nscert/ca/base.py Normal file
View File

@@ -0,0 +1,13 @@
"""Base CA adapter interface."""
from typing import Protocol, Optional
class CAAdapter(Protocol):
def submit_csr(self, csr_pem: str, name: str, options: Optional[dict] = None) -> str:
"""Submit a CSR and return a request id or token."""
def poll_status(self, request_id: str, timeout: int = 60) -> str:
"""Poll request status and return a final state string (e.g., 'issued' / 'pending' / 'failed')."""
def download_certificate(self, request_id: str) -> str:
"""Return the issued certificate PEM for the request_id."""

27
legacy/nscert/ca/mock.py Normal file
View File

@@ -0,0 +1,27 @@
"""A simple mock CA adapter for tests and local runs."""
from typing import Optional
from .base import CAAdapter
class MockCA(CAAdapter):
def __init__(self):
self._store = {}
self._counter = 0
def submit_csr(self, csr_pem: str, name: str, options: Optional[dict] = None) -> str:
self._counter += 1
rid = f"mock-{self._counter}"
# store and pretend it's issued immediately for simplicity
self._store[rid] = {
"csr": csr_pem,
"name": name,
"status": "issued",
"cert": f"-----BEGIN CERTIFICATE-----\nMockCertFor:{name}\n-----END CERTIFICATE-----\n",
}
return rid
def poll_status(self, request_id: str, timeout: int = 60) -> str:
return self._store.get(request_id, {}).get("status", "unknown")
def download_certificate(self, request_id: str) -> str:
return self._store.get(request_id, {}).get("cert")

View File

@@ -0,0 +1,35 @@
"""Sectigo CA adapter skeleton.
This file provides a class with the expected interface. Implementing a full
Sectigo integration requires API credentials and network access; this is a
skeleton with TODOs and a clear place to add HTTP calls.
"""
from typing import Optional
from .base import CAAdapter
class SectigoCA(CAAdapter):
def __init__(self, api_base: str = "https://api.sectigo.com", api_key: Optional[str] = None):
self.api_base = api_base
self.api_key = api_key
def submit_csr(self, csr_pem: str, name: str, options: Optional[dict] = None) -> str:
"""Submit CSR to Sectigo and return request id.
TODO: Implement actual HTTP POSTs with authentication and error handling.
"""
raise NotImplementedError("Sectigo submission not implemented yet")
def poll_status(self, request_id: str, timeout: int = 60) -> str:
"""Poll Sectigo for request status.
TODO: implement polling logic using Sectigo APIs.
"""
raise NotImplementedError("Sectigo polling not implemented yet")
def download_certificate(self, request_id: str) -> str:
"""Download issued certificate PEM.
TODO: fetch the issued certificate from Sectigo.
"""
raise NotImplementedError("Sectigo download not implemented yet")

231
legacy/nscert/cli.py Normal file
View File

@@ -0,0 +1,231 @@
"""Minimal CLI for nscert tooling.
Provides a `keygen` subcommand which wraps `nscert.keygen.generate_private_key`.
This is intentionally small and testable: `main(argv)` accepts an argv list.
"""
from __future__ import annotations
import argparse
import sys
from typing import List, Optional
from . import keygen
def _build_subj_from_dict(subject: dict) -> str:
parts = []
for k in ["C", "ST", "L", "O", "OU", "CN"]:
v = subject.get(k)
if v:
parts.append(f"/{k}={v}")
return "".join(parts)
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(prog="nsctl", description="nscert control utility")
sub = p.add_subparsers(dest="command")
keygen_p = sub.add_parser("keygen", help="Generate a private key")
keygen_p.add_argument("--kind", choices=["rsa", "ec"], default="rsa")
keygen_p.add_argument("--bits", type=int, default=4096, help="RSA key bits")
keygen_p.add_argument("--curve", default="prime256v1", help="EC curve")
keygen_p.add_argument("--passphrase", help="Passphrase to encrypt key (insecure on CLI)")
keygen_p.add_argument("--keychain-service", help="Keychain service name to read/write passphrase")
keygen_p.add_argument("--save-passphrase", action="store_true", help="Save provided passphrase into Keychain")
keygen_p.add_argument("--keychain-account", default="nscert", help="Keychain account name when saving passphrase")
keygen_p.add_argument("--out", help="Write key to file instead of stdout")
# csr subcommands
csr_p = sub.add_parser("csr", help="Create and inspect CSRs")
csr_sub = csr_p.add_subparsers(dest="csr_cmd")
csr_create = csr_sub.add_parser("create", help="Create a CSR from a private key file")
csr_create.add_argument("--key-file", required=True, help="Path to the private key PEM")
csr_create.add_argument("--subject", help="Subject string in /C=.../ST=.../CN=... format or as key=value pairs like C=US,ST=CA,CN=example.com")
csr_create.add_argument("--san", action="append", help="SubjectAltName entry (DNS or IP); can be given multiple times")
csr_create.add_argument("--allow-wildcard", action="store_true", help="Allow wildcard SANs without interactive confirmation. Use with caution: wildcards broaden certificate scope; in non-interactive contexts this flag bypasses interactive confirmation.")
csr_create.add_argument("--from-cert", help="Path to an existing certificate to populate subject and SANs from")
csr_create.add_argument("--passphrase", help="Passphrase for encrypted key (insecure on CLI)")
csr_create.add_argument("--out", help="Write CSR to file instead of stdout")
csr_show = csr_sub.add_parser("show", help="Show CSR details (like SANs)")
csr_show.add_argument("--csr-file", required=True, help="Path to CSR PEM file")
csr_submit = csr_sub.add_parser("submit", help="Submit a CSR to a CA adapter")
csr_submit.add_argument("--csr-file", required=True, help="CSR PEM file to submit")
csr_submit.add_argument("--ca", choices=["mock", "sectigo"], default="mock", help="CA adapter to use")
csr_submit.add_argument("--name", required=True, help="Name for the cert request")
csr_submit.add_argument("--wait", action="store_true", help="Wait for issuance and download cert")
csr_submit.add_argument("--out", help="Write downloaded cert to file")
csr_submit.add_argument("--allow-wildcard", action="store_true", help="Allow submission of CSRs containing wildcard SANs. Use with caution: wildcards broaden certificate scope and may be unsafe.")
return p
def main(argv: Optional[List[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if args.command == "keygen":
if args.kind == "rsa":
pem = keygen.generate_rsa_key(bits=args.bits, passphrase=args.passphrase, keychain_service=args.keychain_service, save_to_keychain=args.save_passphrase, keychain_account=args.keychain_account)
else:
pem = keygen.generate_ec_key(curve=args.curve, passphrase=args.passphrase, keychain_service=args.keychain_service, save_to_keychain=args.save_passphrase, keychain_account=args.keychain_account)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(pem)
else:
sys.stdout.write(pem)
return 0
if args.command == "csr":
if args.csr_cmd == "create":
# Parse subject: accept both /C=... style or comma separated key=value
subject = {}
subj = args.subject or ""
if subj.startswith("/"):
# convert /C=US/ST=... into dict
for part in subj.split("/"):
if not part:
continue
if "=" in part:
k, v = part.split("=", 1)
subject[k] = v
elif subj:
for part in subj.split(","):
if "=" in part:
k, v = part.split("=", 1)
subject[k.strip()] = v.strip()
csr_pem = None
from . import csr as csrmod
if args.from_cert:
# Populate subject and SANs from an existing cert
csr_pem = csrmod.create_csr_from_cert(args.from_cert, args.key_file, passphrase=args.passphrase)
else:
subj_str = subj if subj and subj.startswith("/") else _build_subj_from_dict(subject) if subject else None
# If no subject and no SANs provided, prompt interactively
if not subj_str and not args.san:
subj_dict, san_list = csrmod.prompt_for_subject_and_sans()
subj_str = _build_subj_from_dict(subj_dict) if subj_dict else None
csr_pem = csrmod.create_csr_from_key(args.key_file, subj_str, sans=san_list, passphrase=args.passphrase)
else:
# Validate SANs supplied on CLI
norm_sans = []
if args.san:
wildcard_sans = []
try:
for s in args.san:
ns = csrmod.normalize_and_validate_san(s)
norm_sans.append(ns)
if ns.startswith("DNS:*.") or "*" in ns:
wildcard_sans.append(ns)
except ValueError as e:
from .term import print_error
print_error(f"Invalid SAN provided: {e}")
return 1
# Handle wildcard SANs: interactive confirmation or explicit allow
if wildcard_sans:
if args.allow_wildcard:
# Use formatted warning helper for reusable behavior
from .term import print_warning
print_warning("WARNING: --allow-wildcard used. Wildcard SANs broaden certificate scope and can be risky.")
else:
# If interactive terminal, confirm each wildcard SAN
try:
is_tty = sys.stdin.isatty()
except Exception:
is_tty = False
if not is_tty:
from .term import print_error
print_error("Wildcard SAN detected in CLI arguments; rerun with --allow-wildcard to allow it in non-interactive mode.")
return 1
# interactive confirm
for w in list(wildcard_sans):
ans = input(f"Wildcard SAN detected: {w}. Type 'yes' to include it, or anything else to exclude: ").strip().lower()
if ans != "yes":
# remove from normalized list
norm_sans = [x for x in norm_sans if x != w]
from .term import print_info
print_info(f"Excluded {w}")
# it as None so OpenSSL will not add subjectAltName.
san_to_use = norm_sans if args.san else None
csr_pem = csrmod.create_csr_from_key(args.key_file, subj_str, sans=san_to_use, passphrase=args.passphrase)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(csr_pem)
else:
sys.stdout.write(csr_pem)
return 0
if args.csr_cmd == "show":
with open(args.csr_file, "r", encoding="utf-8") as f:
csr_pem = f.read()
from . import csr as csrmod
# Rudimentary output: show the CSR text parsed by openssl
import subprocess
openssl = csrmod._openssl_bin()
p = subprocess.run([openssl, "req", "-in", "/dev/stdin", "-noout", "-text"], input=csr_pem.encode("utf-8"), check=True, capture_output=True)
sys.stdout.write(p.stdout.decode("utf-8"))
return 0
if args.csr_cmd == "submit":
with open(args.csr_file, "r", encoding="utf-8") as f:
csr_pem = f.read()
# validate SANs if present and wildcard rules
from . import csr as csrmod
# parse SANs in the CSR via openssl text
import subprocess
openssl = csrmod._openssl_bin()
p = subprocess.run([openssl, "req", "-in", "/dev/stdin", "-noout", "-text"], input=csr_pem.encode("utf-8"), check=True, capture_output=True)
txt = p.stdout.decode("utf-8")
import re
sans = [f"DNS:{m.group(1)}" for m in re.finditer(r"DNS:([^,\s]+)", txt)]
for m in re.finditer(r"IP:?\s*Address:?\s*([^,\s]+)", txt, flags=re.IGNORECASE):
sans.append(f"IP:{m.group(1)}")
# wildcard checks
if any(s.startswith("DNS:*." ) or "*" in s for s in sans) and not args.allow_wildcard:
from .term import print_error
print_error("Wildcard SAN detected in CSR; pass --allow-wildcard to confirm you want to submit it.")
return 1
# dispatch to adapter
from .ca.mock import MockCA
from .ca.sectigo import SectigoCA
if args.ca == "mock":
adapter = MockCA()
else:
adapter = SectigoCA()
req_id = adapter.submit_csr(csr_pem, args.name)
from .term import print_info
print_info(f"Submitted CSR, request id: {req_id}")
if args.wait:
status = adapter.poll_status(req_id, timeout=300)
from .term import print_info
print_info(f"Final status: {status}")
if status == "issued":
cert = adapter.download_certificate(req_id)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(cert)
else:
sys.stdout.write(cert)
else:
from .term import print_error
print_error(f"Request ended with status: {status}")
return 0
parser.print_help()
return 2
if __name__ == "__main__":
raise SystemExit(main())

237
legacy/nscert/csr.py Normal file
View File

@@ -0,0 +1,237 @@
"""CSR creation helpers using the OpenSSL CLI.
Provides utilities to create a CSR from a private key file or PEM content,
supporting Subject fields and SubjectAltName entries.
"""
from typing import List, Optional, Dict
import shutil
import subprocess
import tempfile
import os
class OpenSSLNotFound(Exception):
pass
def _openssl_bin() -> str:
path = shutil.which("openssl")
if not path:
raise OpenSSLNotFound("`openssl` not found in PATH")
return path
def _build_subject_string(subject: Dict[str, str]) -> str:
# Accepts keys like C, ST, L, O, OU, CN
parts = []
for k in ["C", "ST", "L", "O", "OU", "CN"]:
v = subject.get(k)
if v:
parts.append(f"/{k}={v}")
return "".join(parts)
def normalize_and_validate_san(s: str) -> str:
"""Normalize and validate a SAN entry.
Accepts 'dns.example.com', 'IP', or prefixed 'DNS:...' / 'IP:...'.
Returns 'DNS:...' or 'IP:...' normalized string. Raises ValueError on invalid input.
"""
import ipaddress
s = s.strip()
if not s:
raise ValueError("empty SAN")
if s.upper().startswith("DNS:") or s.upper().startswith("IP:"):
prefix, val = s.split(":", 1)
val = val.strip()
else:
val = s
prefix = None
# Try IP first
try:
ipaddress.ip_address(val)
return f"IP:{val}"
except Exception:
pass
# Validate DNS name roughly (allow wildcard prefixes like '*.example.com')
import re
# simplified hostname regex (labels separated by dots)
HOST_RE = re.compile(r"^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*$")
if val.startswith("*."):
rest = val[2:]
if HOST_RE.match(rest):
return f"DNS:{val}"
if HOST_RE.match(val):
return f"DNS:{val}"
raise ValueError(f"Invalid SAN value: {s}")
def create_csr_from_key(key_path: str, subject: Optional[Dict[str, str]] = None, sans: Optional[List[str]] = None, passphrase: Optional[str] = None) -> str:
"""Create a CSR using OpenSSL and return the CSR PEM text.
- `key_path` must point to a private key file (PEM); if it is encrypted,
supply `passphrase` which will be passed to OpenSSL via `-passin`.
- `subject` may be provided either as a dict with fields like C, ST, L, O, OU, CN
or as a subject string starting with a leading slash, e.g. '/C=US/ST=CA/CN=example.com'.
- `sans` is a list of DNS names/IPs for SubjectAltName.
"""
openssl = _openssl_bin()
if isinstance(subject, dict):
subj = _build_subject_string(subject)
else:
subj = subject
# If sans look like ['DNS:...','IP:...'] normalize to plain names for config
norm_sans = []
if sans:
for s in sans:
# Normalize and validate; allow ValueError to bubble to caller
ns = normalize_and_validate_san(s)
norm_sans.append(ns)
with tempfile.TemporaryDirectory() as td:
conf_path = os.path.join(td, "csr.conf")
csr_path = os.path.join(td, "req.csr")
# Build minimal OpenSSL config with SANs if provided
conf_lines = ["[ req ]", "distinguished_name = req_distinguished_name", "prompt = no"]
if norm_sans:
conf_lines.append("req_extensions = v3_req")
conf_lines.append("")
conf_lines.append("[ req_distinguished_name ]")
# No need to fill DN here when using -subj, but keep the section present
conf_lines.append("")
if norm_sans:
conf_lines.append("[ v3_req ]")
conf_lines.append(f"subjectAltName = {', '.join(norm_sans)}")
with open(conf_path, "w", encoding="utf-8") as f:
f.write("\n".join(conf_lines))
cmd = [openssl, "req", "-new", "-key", key_path, "-out", csr_path, "-config", conf_path]
if subj:
cmd += ["-subj", subj]
if passphrase:
cmd += ["-passin", f"pass:{passphrase}"]
subprocess.run(cmd, check=True, capture_output=True)
with open(csr_path, "r", encoding="utf-8") as f:
return f.read()
def csr_has_san(csr_pem: str, san: str) -> bool:
"""Return True if the CSR PEM contains the provided SAN entry.
This uses `openssl req -in - -noout -text` to parse the CSR contents.
"""
openssl = _openssl_bin()
p = subprocess.run([openssl, "req", "-in", "/dev/stdin", "-noout", "-text"], input=csr_pem.encode("utf-8"), check=True, capture_output=True)
out = p.stdout.decode("utf-8")
# Accept a few representations: 'DNS:...' or 'IP:...' -> just look for the name/value
if san.startswith("DNS:"):
return san[4:] in out
if san.startswith("IP:"):
return san[3:] in out
return san in out
def extract_subject_and_sans_from_cert(cert_pem: str) -> (Dict[str, str], List[str]):
"""Extract subject fields and SANs from a certificate PEM using openssl.
Returns (subject_dict, san_list)
"""
openssl = _openssl_bin()
# Get subject in RFC2253 style for easier parsing
p = subprocess.run([openssl, "x509", "-in", "/dev/stdin", "-noout", "-subject", "-nameopt", "RFC2253"], input=cert_pem.encode("utf-8"), check=True, capture_output=True)
subj_out = p.stdout.decode("utf-8").strip()
# subj_out is like: "subject=CN=example.com,O=Example,C=US"
subj = subj_out.split("=", 1)[1] if "=" in subj_out else ""
subject_parts = {}
for part in subj.split(","):
if "=" in part:
k, v = part.split("=", 1)
subject_parts[k.strip()] = v.strip()
# Get SANs from the text representation
p2 = subprocess.run([openssl, "x509", "-in", "/dev/stdin", "-noout", "-text"], input=cert_pem.encode("utf-8"), check=True, capture_output=True)
txt = p2.stdout.decode("utf-8")
sans = []
import re
for m in re.finditer(r"DNS:([^,\s]+)", txt):
sans.append(f"DNS:{m.group(1)}")
# OpenSSL sometimes prints IPs as 'IP:' or 'IP Address: '
for m in re.finditer(r"IP:?\s*Address:?\s*([^,\s]+)", txt, flags=re.IGNORECASE):
sans.append(f"IP:{m.group(1)}")
for m in re.finditer(r"IP:([^,\s]+)", txt):
sans.append(f"IP:{m.group(1)}")
return subject_parts, sans
def create_csr_from_cert(cert_path: str, key_path: str, passphrase: Optional[str] = None) -> str:
"""Create a CSR based on an existing certificate's subject and SANs, signing with `key_path`."""
with open(cert_path, "r", encoding="utf-8") as f:
cert_pem = f.read()
subject, sans = extract_subject_and_sans_from_cert(cert_pem)
# convert subject dict into subject string
subj_str = _build_subject_string(subject) if subject else None
return create_csr_from_key(key_path, subj_str, sans=sans, passphrase=passphrase)
def prompt_for_subject_and_sans() -> (Dict[str, str], List[str]):
"""Interactively prompt the user for subject fields and SANs.
Returns a (subject_dict, san_list) tuple.
"""
fields = ["C", "ST", "L", "O", "OU", "CN"]
subject: Dict[str, str] = {}
from .term import print_info
print_info("Enter subject fields (press Enter to skip a field)")
for f in fields:
try:
val = input(f"{f}: ").strip()
except EOFError:
val = ""
if val:
subject[f] = val
print_info("Enter SANs (one per line). Examples: 'www.example.com' or '10.0.0.1'. Leave blank to finish.")
sans: List[str] = []
while True:
try:
s = input("SAN: ").strip()
except EOFError:
break
if not s:
break
try:
norm = normalize_and_validate_san(s)
except ValueError as e:
from .term import print_error
print_error(f"Invalid SAN: {e}. Try again.")
continue
# If this is a wildcard SAN, ask for explicit confirmation
if norm.startswith("DNS:*."):
ans = input(
f"Wildcard SAN detected: {norm}. Wildcard SANs (e.g., *.example.com) broaden certificate scope and can be risky. Type 'yes' to include it, or anything else to exclude: "
).strip().lower()
if ans != "yes":
from .term import print_info
print_info(f"Excluded {norm}")
continue
else:
from .term import print_info
print_info(f"Included {norm}")
# store the normalized SAN
sans.append(norm)
return subject, sans

120
legacy/nscert/keygen.py Normal file
View File

@@ -0,0 +1,120 @@
"""Key generation helpers using the OpenSSL CLI.
This module intentionally uses the system `openssl` command for portability on
macOS systems where the `cryptography` library may not be available by
default.
Functions return the PEM text of the generated private key.
"""
from typing import Optional
import shutil
import subprocess
class OpenSSLNotFound(Exception):
pass
def _openssl_bin() -> str:
path = shutil.which("openssl")
if not path:
raise OpenSSLNotFound("`openssl` not found in PATH")
return path
def generate_rsa_key(bits: int = 4096, passphrase: Optional[str] = None, cipher: Optional[str] = "des3", keychain_service: Optional[str] = None, save_to_keychain: bool = False, keychain_account: str = "nscert") -> str:
"""Generate an RSA private key in PEM format.
If `passphrase` is provided or available in `keychain_service`, the PEM
will be encrypted with the provided `cipher` (defaults to 3DES via
OpenSSL's `des3` v2 option).
If `save_to_keychain` is True and `keychain_service` is provided and a
passphrase was supplied by the caller, the passphrase will be saved into
the macOS Keychain using `nscert.storage.keychain_set`.
Returns PEM text.
"""
# Optionally save provided passphrase to keychain
if passphrase and keychain_service and save_to_keychain:
try:
from . import storage
storage.keychain_set(keychain_service, passphrase, account=keychain_account)
except Exception:
# Best-effort only; do not fail the key generation on keychain errors
pass
# Resolve passphrase via keychain if requested and not provided
if not passphrase and keychain_service:
try:
from . import storage
passphrase = storage.keychain_get(keychain_service)
except Exception:
pass
openssl = _openssl_bin()
# Generate an unencrypted private key into stdout using genpkey
gen_cmd = [openssl, "genpkey", "-algorithm", "RSA", "-pkeyopt", f"rsa_keygen_bits:{bits}", "-outform", "PEM"]
gen = subprocess.run(gen_cmd, check=True, capture_output=True)
private_pem = gen.stdout
if passphrase:
# Convert to PKCS#8 and encrypt
# Use -v2 <cipher> for modern encryption (des3 corresponds to 3DES)
topk8_cmd = [openssl, "pkcs8", "-topk8", "-v2", cipher, "-passout", f"pass:{passphrase}", "-outform", "PEM"]
topk8 = subprocess.run(topk8_cmd, input=private_pem, check=True, capture_output=True)
return topk8.stdout.decode("utf-8")
return private_pem.decode("utf-8")
def generate_ec_key(curve: str = "prime256v1", passphrase: Optional[str] = None, cipher: Optional[str] = "des3", keychain_service: Optional[str] = None, save_to_keychain: bool = False, keychain_account: str = "nscert") -> str:
"""Generate an EC private key (PEM) for given curve name (e.g., prime256v1 / secp384r1).
If `passphrase` is provided or available in `keychain_service`, the PEM
will be encrypted using PKCS#8 v2.
If `save_to_keychain` is True and `keychain_service` is provided and a
passphrase was supplied by the caller, the passphrase will be saved into
the macOS Keychain using `nscert.storage.keychain_set`.
"""
# Optionally save provided passphrase to keychain
if passphrase and keychain_service and save_to_keychain:
try:
from . import storage
storage.keychain_set(keychain_service, passphrase, account=keychain_account)
except Exception:
pass
# Resolve passphrase via keychain if requested and not provided
if not passphrase and keychain_service:
try:
from . import storage
passphrase = storage.keychain_get(keychain_service)
except Exception:
pass
openssl = _openssl_bin()
gen_cmd = [openssl, "ecparam", "-name", curve, "-genkey", "-noout", "-outform", "PEM"]
gen = subprocess.run(gen_cmd, check=True, capture_output=True)
private_pem = gen.stdout
if passphrase:
topk8_cmd = [openssl, "pkcs8", "-topk8", "-v2", cipher, "-passout", f"pass:{passphrase}", "-outform", "PEM"]
topk8 = subprocess.run(topk8_cmd, input=private_pem, check=True, capture_output=True)
return topk8.stdout.decode("utf-8")
return private_pem.decode("utf-8")
def generate_private_key(kind: str = "rsa", **kwargs) -> str:
"""Generic helper to generate a private key of a given `kind` ('rsa' or 'ec')."""
kind = kind.lower()
if kind == "rsa":
return generate_rsa_key(**kwargs)
if kind == "ec":
return generate_ec_key(**kwargs)
raise ValueError("Unsupported key kind: use 'rsa' or 'ec'")

View File

@@ -0,0 +1,32 @@
"""Minimal Netscaler Console helpers.
This module provides a thin wrapper around the extractor and a clear place to
add Console-specific parsing and upload helpers later.
"""
from typing import Optional
from .utils import extract_pem_from_json
def extract_csr_text(response_json: dict) -> Optional[str]:
"""Return CSR PEM text from a Console/device response JSON.
Prioritize the Console `ns_ssl_csr` response shape, then fall back to a
recursive search for the PEM block.
"""
if not isinstance(response_json, dict):
return extract_pem_from_json(response_json)
# Prefer well-known ns_ssl_csr array
ns_ssl_csr = response_json.get("ns_ssl_csr")
if isinstance(ns_ssl_csr, list):
for entry in ns_ssl_csr:
if isinstance(entry, dict):
csr = entry.get("csr")
if csr:
found = extract_pem_from_json(csr)
if found:
return found
# Fallback general search
return extract_pem_from_json(response_json)

62
legacy/nscert/storage.py Normal file
View File

@@ -0,0 +1,62 @@
"""Credential storage abstraction for macOS Keychain.
Provides small helpers to get/set generic passwords using the `security`
command on macOS. Functions are intentionally simple and easily testable via
stubbing subprocess calls.
"""
from typing import Optional
import subprocess
import shlex
import getpass
def keychain_get(service: str) -> Optional[str]:
"""Return the password for `service` from macOS Keychain, or None if not found."""
try:
# 'security find-generic-password -s <service> -w' prints the password
completed = subprocess.run(["security", "find-generic-password", "-s", service, "-w"], check=True, capture_output=True)
return completed.stdout.decode("utf-8").rstrip("\n")
except subprocess.CalledProcessError:
return None
def keychain_set(service: str, password: str, account: str = "nscert") -> bool:
"""Set a generic password in the Keychain for the given service."""
# Use -U to update if exists
try:
subprocess.run(["security", "add-generic-password", "-a", account, "-s", service, "-w", password, "-U"], check=True, capture_output=True)
return True
except subprocess.CalledProcessError:
return False
def keychain_delete(service: str) -> bool:
try:
subprocess.run(["security", "delete-generic-password", "-s", service], check=True, capture_output=True)
return True
except subprocess.CalledProcessError:
return False
def get_or_prompt_passphrase(service: Optional[str], prompt: Optional[str] = None) -> str:
"""Get passphrase from keychain or prompt the user interactively.
If `service` is provided, on a user-entered passphrase we attempt to store
it back into the Keychain for future use.
"""
if service:
existing = keychain_get(service)
if existing:
return existing
# Fallback to interactive prompt
text = getpass.getpass(prompt or "Enter passphrase for private key: ")
if service and text:
# best effort: set into keychain but ignore failures
try:
keychain_set(service, text)
except Exception:
pass
return text

73
legacy/nscert/term.py Normal file
View File

@@ -0,0 +1,73 @@
"""Terminal/TTY helpers for CLI formatting and safe output.
Provides small helpers to format and print warnings with optional ANSI styling
when the target stream is a TTY.
"""
from typing import Optional, TextIO
import sys
def format_warning(msg: str, stream: Optional[TextIO] = None) -> str:
"""Return a formatted warning string.
If ``stream`` is a tty (supports ``.isatty()``) the message will be wrapped
with ANSI bold yellow codes for prominence; otherwise the plain message is
returned.
"""
if stream is None:
stream = sys.stderr
try:
is_tty = stream.isatty()
except Exception:
is_tty = False
if is_tty:
return f"\033[1;33m{msg}\033[0m"
return msg
def print_warning(msg: str, stream: Optional[TextIO] = None) -> None:
"""Write a warning message followed by a newline to ``stream`` (defaults to stderr)."""
if stream is None:
stream = sys.stderr
stream.write(format_warning(msg, stream) + "\n")
# Additional semantic helpers
def format_error(msg: str, stream: Optional[TextIO] = None) -> str:
"""Return a formatted error string (bold red on TTY)."""
if stream is None:
stream = sys.stderr
try:
is_tty = stream.isatty()
except Exception:
is_tty = False
if is_tty:
return f"\033[1;31m{msg}\033[0m"
return msg
def print_error(msg: str, stream: Optional[TextIO] = None) -> None:
"""Write an error message followed by a newline to ``stream`` (defaults to stderr)."""
if stream is None:
stream = sys.stderr
stream.write(format_error(msg, stream) + "\n")
def format_info(msg: str, stream: Optional[TextIO] = None) -> str:
"""Return a formatted info string (bold green on TTY)."""
if stream is None:
stream = sys.stderr
try:
is_tty = stream.isatty()
except Exception:
is_tty = False
if is_tty:
return f"\033[1;32m{msg}\033[0m"
return msg
def print_info(msg: str, stream: Optional[TextIO] = None) -> None:
"""Write an info message followed by a newline to ``stream`` (defaults to stderr)."""
if stream is None:
stream = sys.stderr
stream.write(format_info(msg, stream) + "\n")

62
legacy/nscert/utils.py Normal file
View File

@@ -0,0 +1,62 @@
"""Utility helpers for nscert package.
Includes a robust PEM extractor that searches arbitrary Console JSON payloads
for a PEM CSR block (-----BEGIN CERTIFICATE REQUEST----- ...
-----END CERTIFICATE REQUEST-----).
"""
from typing import Optional, Any, TextIO
import re
import sys
PEM_RE = re.compile(r"-----BEGIN CERTIFICATE REQUEST-----(?:.|\n)+?-----END CERTIFICATE REQUEST-----", re.DOTALL)
# Compatibility exports: delegate terminal formatting to nscert.term
from .term import format_warning, print_warning, format_error, print_error, format_info, print_info # re-exported for backwards compatibility
def extract_pem_from_json(obj: Any) -> Optional[str]:
"""Recursively search `obj` (dict/list/str) for the first PEM CSR block.
Returns the matched PEM string or None if not found.
"""
if isinstance(obj, str):
m = PEM_RE.search(obj)
if m:
return m.group(0)
return None
if isinstance(obj, dict):
# check common CSR locations quickly
# e.g., {"ns_ssl_csr": [{"csr": "-----BEGIN..."}]}
if "ns_ssl_csr" in obj:
val = obj.get("ns_ssl_csr")
if isinstance(val, list):
for item in val:
if isinstance(item, dict):
csr = item.get("csr")
if csr:
m = PEM_RE.search(csr)
if m:
return m.group(0)
# generic recursive walk
for v in obj.values():
res = extract_pem_from_json(v)
if res:
return res
if isinstance(obj, list):
# If it's a list of strings (rows), join them and search the whole block
if all(isinstance(i, str) for i in obj):
joined = "\n".join(obj)
m = PEM_RE.search(joined)
if m:
return m.group(0)
for item in obj:
res = extract_pem_from_json(item)
if res:
return res
return None