diff --git a/simple-ca.py b/simple-ca.py index 6de34de..a786600 100755 --- a/simple-ca.py +++ b/simple-ca.py @@ -24,6 +24,7 @@ # This module requires Python 3.8+ and OpenSSL to be installed on the system. import argparse +import json import os import re import subprocess @@ -31,11 +32,34 @@ import sys OPENSSL = "/usr/bin/openssl" +CONFIG_FILE = "simple-ca.json" + def _err(msg): print(f"ERROR: {msg}", file=sys.stderr) +def _load_config(ca_dir) -> dict: + path = os.path.join(ca_dir, CONFIG_FILE) + if not os.path.isfile(path): + return {} + try: + with open(path, "r") as f: + return json.load(f) + except (json.JSONDecodeError, OSError) as e: + print(f"WARNING: could not read {path}: {e}", file=sys.stderr) + return {} + + +def _save_config(ca_dir, patch: dict): + path = os.path.join(ca_dir, CONFIG_FILE) + cfg = _load_config(ca_dir) + cfg.update(patch) + with open(path, "w") as f: + json.dump(cfg, f, indent=2) + f.write("\n") + + def _rebuild_ca_bundle(ca_dir): """Write ca_bundle.pem = root cert + any issuing CA certs in this dir.""" bundle_path = os.path.join(ca_dir, "ca_bundle.pem") @@ -68,11 +92,6 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None): _err("CA name is required.") return False - aia_file = os.path.join(ca_dir, "aia_base_url.txt") - if not aia_base_url and os.path.isfile(aia_file): - with open(aia_file, "r") as f: - aia_base_url = f.read().strip() - ca_file_prefix = issuing_ca or "ca" root_ca_cert = "ca_cert.pem" root_ca_key = "ca_key.pem" @@ -114,11 +133,7 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None): return False _rebuild_ca_bundle(ca_dir) - - if aia_base_url: - with open(aia_file, "w") as f: - f.write(aia_base_url) - + _save_config(ca_dir, {"aia_base_url": aia_base_url} if aia_base_url else {}) return True if not os.path.isfile(ca_cert_path) or not os.path.isfile(ca_key_path): @@ -152,11 +167,7 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None): return False _rebuild_ca_bundle(ca_dir) - - if aia_base_url: - with open(aia_file, "w") as f: - f.write(aia_base_url) - + _save_config(ca_dir, {"aia_base_url": aia_base_url} if aia_base_url else {}) return True @@ -182,7 +193,7 @@ def _pipe(cmd1, cmd2): def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None, - issuing_ca=None, days=365): + issuing_ca=None, days=365, aia_base_url=None): if issuing_ca == "ca": _err("--issuing-ca cannot be 'ca' as it is reserved for the root CA.") return False @@ -190,11 +201,7 @@ def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None, ca_file_prefix = issuing_ca or "ca" ca_dir = ca_dir or cert_dir - aia_base_url_file = os.path.join(ca_dir, "aia_base_url.txt") - aia_url = "" - if os.path.isfile(aia_base_url_file): - with open(aia_base_url_file, "r") as f: - aia_url = f"{f.read().strip()}/{ca_file_prefix}_cert.crt" + aia_url = f"{aia_base_url}/{ca_file_prefix}_cert.crt" if aia_base_url else "" ca_cert = f"{ca_file_prefix}_cert.pem" ca_key = f"{ca_file_prefix}_key.pem" @@ -370,29 +377,29 @@ def _build_parser(): sub = parser.add_subparsers(dest="command", required=True) p_ca = sub.add_parser("make-ca", help="Create a root or issuing CA.") - p_ca.add_argument("--days", type=int, default=3650, help="Validity period in days (default: 3650)") - p_ca.add_argument("--issuing-ca", help="Specify the issuing CA") - p_ca.add_argument("--aia-base-url", help="Specify the AIA base URL") + p_ca.add_argument("--days", type=int, default=None, help="Validity period in days (default: 3650)") + p_ca.add_argument("--issuing-ca", default=None, help="Specify the issuing CA") + p_ca.add_argument("--aia-base-url", default=None, help="Specify the AIA base URL") p_ca.add_argument("--ca-dir", help="Directory to store the CA files") - p_ca.add_argument("--openssl", default=OPENSSL, metavar="PATH", + p_ca.add_argument("--openssl", default=None, metavar="PATH", help=f"Path to the openssl binary (default: {OPENSSL})") p_ca.add_argument("ca_name", help="Name of the CA") p_cert = sub.add_parser("make-cert", help="Create a server/client certificate.") p_cert.add_argument("--ca-dir", help="Directory of the CA") - p_cert.add_argument("--issuing-ca", help="Specify the issuing CA") - p_cert.add_argument("--days", type=int, default=365, help="Validity period in days (default: 365)") + p_cert.add_argument("--issuing-ca", default=None, help="Specify the issuing CA") + p_cert.add_argument("--days", type=int, default=None, help="Validity period in days (default: 365)") p_cert.add_argument("--cert-dir", help="Directory to store the certificate files") - p_cert.add_argument("--openssl", default=OPENSSL, metavar="PATH", + p_cert.add_argument("--openssl", default=None, metavar="PATH", help=f"Path to the openssl binary (default: {OPENSSL})") p_cert.add_argument("subject_name", help="Subject name for the certificate") p_cert.add_argument("sans", nargs="*", help="Subject Alternative Names (SANs) for the certificate") p_pfx = sub.add_parser("make-pfx", help="Create a PKCS#12 (PFX) bundle.") - p_pfx.add_argument("--issuing-ca", help="Specify the issuing CA") + p_pfx.add_argument("--issuing-ca", default=None, help="Specify the issuing CA") p_pfx.add_argument("--ca-dir", help="Directory of the CA") p_pfx.add_argument("--password", help="Password for the PFX file") - p_pfx.add_argument("--openssl", default=OPENSSL, metavar="PATH", + p_pfx.add_argument("--openssl", default=None, metavar="PATH", help=f"Path to the openssl binary (default: {OPENSSL})") p_pfx.add_argument("path", help="Path to the certificate file") @@ -404,28 +411,38 @@ def main(argv=None): parser = _build_parser() args = parser.parse_args(argv) - OPENSSL = args.openssl ca_dir = args.ca_dir or os.environ.get("SIMPLE_CA_DIR") or os.getcwd() + cfg = _load_config(ca_dir) + + OPENSSL = args.openssl or cfg.get("openssl", OPENSSL) + issuing_ca = args.issuing_ca or cfg.get("issuing_ca") + aia_base_url = getattr(args, "aia_base_url", None) or cfg.get("aia_base_url") + + days_cfg = cfg.get("days", {}) + if args.command == "make-ca": + days = args.days or days_cfg.get("ca", 3650) ok = make_ca( ca_dir, args.ca_name, - days=args.days, - issuing_ca=args.issuing_ca, - aia_base_url=args.aia_base_url, + days=days, + issuing_ca=issuing_ca, + aia_base_url=aia_base_url, ) elif args.command == "make-cert": + days = args.days or days_cfg.get("cert", 365) ok = make_cert( args.cert_dir, args.subject_name, sans=args.sans, ca_dir=ca_dir, - issuing_ca=args.issuing_ca, - days=args.days, + issuing_ca=issuing_ca, + days=days, + aia_base_url=aia_base_url, ) elif args.command == "make-pfx": ok = make_pfx( args.path, ca_dir, - issuing_ca=args.issuing_ca, + issuing_ca=issuing_ca, password=args.password, ) else: