Remove simple-ca module and its associated files
/ test-python (push) Successful in 49s
/ test-go (push) Failing after 4m44s

- Deleted go.mod and go.sum files for the simple-ca module.
- Removed main.go file containing the implementation of the simple-ca tool.
This commit is contained in:
2026-05-24 16:47:43 +02:00
parent 0d61919162
commit cb858a96b1
3 changed files with 271 additions and 104 deletions
-39
View File
@@ -56,45 +56,6 @@ make_pfx --ca-dir <ca_directory> [--issuing-ca <file_prefix>] --path <pfx_file_p
- `--path <pfx_file_path>`: The path where the generated PFX file will be saved. - `--path <pfx_file_path>`: The path where the generated PFX file will be saved.
- `--password <pfx_password>`: Optional. The custom password to protect the PFX, instead of the default `changeit`. - `--password <pfx_password>`: Optional. The custom password to protect the PFX, instead of the default `changeit`.
## Go binary
A Go port with the same feature set lives in [`src/simple-ca`](src/simple-ca). It compiles to a single self-contained binary (~56 MB) with no runtime dependencies, and exposes `make-ca`, `make-cert`, and `make-pfx` as subcommands mirroring the Bash flag names.
### Build for the host platform
```bash
cd src/simple-ca
go build -o simple-ca .
./simple-ca --help
```
### Cross-compile
Go builds statically linked binaries for any target from any host:
```bash
cd src/simple-ca
# Linux
GOOS=linux GOARCH=amd64 go build -o simple-ca-linux-amd64 .
GOOS=linux GOARCH=arm64 go build -o simple-ca-linux-arm64 .
# macOS
GOOS=darwin GOARCH=amd64 go build -o simple-ca-darwin-amd64 .
GOOS=darwin GOARCH=arm64 go build -o simple-ca-darwin-arm64 .
# Windows
GOOS=windows GOARCH=amd64 go build -o simple-ca-windows-amd64.exe .
```
### Usage
```bash
simple-ca make-ca [--days N] [--issuing-ca PREFIX] [--aia-base-url URL] CA_DIR CA_NAME
simple-ca make-cert [--ca-dir DIR] [--days N] [--issuing-ca PREFIX] CERT_DIR SUBJECT [SAN...]
simple-ca make-pfx --ca-dir DIR [--issuing-ca PREFIX] --path CERT_PATH [--password PASS]
```
## generate-mobileconfig.py ## generate-mobileconfig.py
`generate-mobileconfig.py` generates Apple `.mobileconfig` profiles for distributing CA certificates and optionally client certificates and IKEv2 VPN configuration to Apple devices (macOS / iOS / iPadOS). `generate-mobileconfig.py` generates Apple `.mobileconfig` profiles for distributing CA certificates and optionally client certificates and IKEv2 VPN configuration to Apple devices (macOS / iOS / iPadOS).
+66 -13
View File
@@ -21,8 +21,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
# This script runs integration tests against one or more simple-ca implementations. # This script runs integration tests against the simple-ca Python implementation.
# Usage: run-tests.sh [python|go|all] (default: all) # Usage: run-tests.sh [python|all] (default: all)
set -e set -e
TEST_TARGET="${1:-all}" TEST_TARGET="${1:-all}"
@@ -149,6 +149,67 @@ run_pfx_algorithm_tests() {
fi fi
} }
# run_crl_tests NAME MAKE_CA_CMD MAKE_CERT_CMD MAKE_PFX_CMD REVOKE_CMD MAKE_CRL_CMD
run_crl_tests() {
local NAME="$1"
local MAKE_CA_CMD="$2"
local MAKE_CERT_CMD="$3"
local REVOKE_CMD="$4"
local MAKE_CRL_CMD="$5"
echo
echo "--- [$NAME] CRL tests ---"
clean_up_test_dir
# Build a two-level CA hierarchy
$MAKE_CA_CMD --ca-dir "$CA_DIR" "CRL Test CA"
$MAKE_CA_CMD --ca-dir "$CA_DIR" --issuing-ca "issuing_ca" "Issuing CA"
# Issue two certs; revoke the first; keep the second active
$MAKE_CERT_CMD --ca-dir "$CA_DIR" --cert-dir "$CERT_DIR" --issuing-ca "issuing_ca" \
"alice" "alice.example.com"
$MAKE_CERT_CMD --ca-dir "$CA_DIR" --cert-dir "$CERT_DIR" --issuing-ca "issuing_ca" \
"bob" "bob.example.com"
$REVOKE_CMD --ca-dir "$CA_DIR" --issuing-ca "issuing_ca" "$CERT_DIR/alice_cert.pem"
# Generate CRL for the issuing CA
$MAKE_CRL_CMD --ca-dir "$CA_DIR" --issuing-ca "issuing_ca"
[[ -f "$CA_DIR/issuing_ca/crl.pem" ]] || { echo "ERROR: issuing_ca/crl.pem not created" >&2; exit 1; }
# alice's serial must appear in the issuing CA CRL
CRL_TEXT=$(openssl crl -in "$CA_DIR/issuing_ca/crl.pem" -noout -text 2>/dev/null)
ALICE_SERIAL=$(openssl x509 -in "$CERT_DIR/alice_cert.pem" -noout -serial | cut -d= -f2)
if echo "$CRL_TEXT" | grep -qi "$ALICE_SERIAL"; then
echo "CRL [issuing_ca]: alice's serial found — OK"
else
echo "ERROR: CRL [issuing_ca]: alice's serial not found in CRL" >&2
echo "$CRL_TEXT" >&2
exit 1
fi
# bob's serial must NOT appear in the issuing CA CRL
BOB_SERIAL=$(openssl x509 -in "$CERT_DIR/bob_cert.pem" -noout -serial | cut -d= -f2)
if echo "$CRL_TEXT" | grep -qi "$BOB_SERIAL"; then
echo "ERROR: CRL [issuing_ca]: bob's serial unexpectedly found in CRL" >&2
exit 1
else
echo "CRL [issuing_ca]: bob's serial absent — OK"
fi
# Root CA CRL should be empty (no revoctions at root level)
$MAKE_CRL_CMD --ca-dir "$CA_DIR"
[[ -f "$CA_DIR/crl.pem" ]] || { echo "ERROR: root crl.pem not created" >&2; exit 1; }
ROOT_CRL_TEXT=$(openssl crl -in "$CA_DIR/crl.pem" -noout -text 2>/dev/null)
if echo "$ROOT_CRL_TEXT" | grep -q "No Revoked Certificates"; then
echo "CRL [root]: empty — OK"
else
echo "ERROR: CRL [root]: expected empty CRL" >&2
echo "$ROOT_CRL_TEXT" >&2
exit 1
fi
}
# Uses ;;& to fall through to subsequent patterns so 'all' matches every block. # Uses ;;& to fall through to subsequent patterns so 'all' matches every block.
case "$TEST_TARGET" in case "$TEST_TARGET" in
python|all) python|all)
@@ -156,20 +217,12 @@ case "$TEST_TARGET" in
PY_PREFIX="python3 $SCRIPT_DIR/simple-ca.py" PY_PREFIX="python3 $SCRIPT_DIR/simple-ca.py"
run_flow "python" "$PY_PREFIX make-ca" "$PY_PREFIX make-cert" "$PY_PREFIX make-pfx" run_flow "python" "$PY_PREFIX make-ca" "$PY_PREFIX make-cert" "$PY_PREFIX make-pfx"
run_pfx_algorithm_tests "python" "$PY_PREFIX make-ca" "$PY_PREFIX make-cert" "$PY_PREFIX make-pfx" run_pfx_algorithm_tests "python" "$PY_PREFIX make-ca" "$PY_PREFIX make-cert" "$PY_PREFIX make-pfx"
run_crl_tests "python" "$PY_PREFIX make-ca" "$PY_PREFIX make-cert" "$PY_PREFIX revoke-cert" "$PY_PREFIX make-crl"
;;& ;;&
go|all) python|all)
command -v go >/dev/null || { echo "ERROR: go not found" >&2; exit 1; }
GO_SRC="$SCRIPT_DIR/src/simple-ca"
GO_BIN="$GO_SRC/simple-ca"
echo "Building Go binary..."
(cd "$GO_SRC" && go build -o simple-ca .)
run_flow "go" "$GO_BIN make-ca" "$GO_BIN make-cert" "$GO_BIN make-pfx"
run_pfx_algorithm_tests "go" "$GO_BIN make-ca" "$GO_BIN make-cert" "$GO_BIN make-pfx"
;;&
python|go|all)
;; ;;
*) *)
echo "ERROR: unknown target '$TEST_TARGET' (expected: python|go|all)" >&2 echo "ERROR: unknown target '$TEST_TARGET' (expected: python|all)" >&2
exit 1 exit 1
;; ;;
esac esac
+205 -52
View File
@@ -24,14 +24,17 @@
# This module requires Python 3.8+ and OpenSSL to be installed on the system. # This module requires Python 3.8+ and OpenSSL to be installed on the system.
import argparse import argparse
import datetime
import json import json
import os import os
import re import re
import subprocess import subprocess
import sys import sys
import tempfile
OPENSSL = "openssl" OPENSSL = "openssl"
class Config: class Config:
FILE = "simple-ca.json" FILE = "simple-ca.json"
@@ -56,6 +59,26 @@ class Config:
self._data.update(patch) self._data.update(patch)
self._save() self._save()
def append_history(self, ca_key: str, entry: dict):
history = self._data.get("history", {})
history.setdefault(ca_key, []).append(entry)
self._data["history"] = history
self._save()
def revoke_in_history(self, ca_key: str, serial: str, revoked_at: str):
"""Mark a certificate as revoked. Returns True if newly revoked,
False if already revoked, None if serial not found."""
history = self._data.get("history", {})
for entry in history.get(ca_key, []):
if entry.get("serial") == serial:
if "revoked" in entry:
return False
entry["revoked"] = revoked_at
self._data["history"] = history
self._save()
return True
return None
def _save(self): def _save(self):
with open(self._path, "w") as f: with open(self._path, "w") as f:
json.dump(self._data, f, indent=2) json.dump(self._data, f, indent=2)
@@ -65,12 +88,15 @@ class Config:
_config: Config _config: Config
def _now() -> str:
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _err(msg): def _err(msg):
print(f"ERROR: {msg}", file=sys.stderr) print(f"ERROR: {msg}", file=sys.stderr)
def _rebuild_ca_bundle(ca_dir): def _rebuild_ca_bundle(ca_dir):
"""Write ca_bundle.pem = root cert + registered subordinate CA certs."""
bundle_path = os.path.join(ca_dir, "ca_bundle.pem") bundle_path = os.path.join(ca_dir, "ca_bundle.pem")
parts = [] parts = []
root = os.path.join(ca_dir, "ca_cert.pem") root = os.path.join(ca_dir, "ca_cert.pem")
@@ -86,7 +112,54 @@ def _rebuild_ca_bundle(ca_dir):
f.write(b"".join(parts)) f.write(b"".join(parts))
def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None): def _read_serial(cert_path) -> str:
result = subprocess.run(
[OPENSSL, "x509", "-in", cert_path, "-noout", "-serial"],
capture_output=True, text=True,
)
return result.stdout.strip().split("=", 1)[-1]
def _read_expiry(cert_path) -> str:
"""Return the certificate notAfter date as an ISO 8601 UTC string."""
result = subprocess.run(
[OPENSSL, "x509", "-in", cert_path, "-noout", "-enddate"],
capture_output=True, text=True,
)
raw = result.stdout.strip().split("=", 1)[-1]
raw = " ".join(raw.split()) # normalize whitespace ("May 4" → "May 4")
dt = datetime.datetime.strptime(raw, "%b %d %H:%M:%S %Y GMT")
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
def _iso_to_asn1(iso: str) -> str:
"""Convert "2026-05-24T14:28:10Z""260524142810Z" for OpenSSL index.txt."""
dt = datetime.datetime.strptime(iso, "%Y-%m-%dT%H:%M:%SZ")
return dt.strftime("%y%m%d%H%M%SZ")
_IP_RE = re.compile(r"^[0-9]{1,3}(\.[0-9]{1,3}){3}$")
_DNS_RE = re.compile(r"^[a-z0-9-]+(\.[a-z0-9-]+)*$")
def _is_ip(value):
return bool(_IP_RE.match(value))
def _is_dns(value):
return bool(_DNS_RE.match(value))
def _pipe(cmd1, cmd2):
p1 = subprocess.Popen(cmd1, stdout=subprocess.PIPE)
p2 = subprocess.Popen(cmd2, stdin=p1.stdout)
p1.stdout.close()
p2.communicate()
p1.wait()
return p1.returncode == 0 and p2.returncode == 0
def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, ca_publish_base_url=None):
if issuing_ca == "ca": if issuing_ca == "ca":
_err("--issuing-ca cannot be 'ca' as it is reserved for the root CA.") _err("--issuing-ca cannot be 'ca' as it is reserved for the root CA.")
return False return False
@@ -111,9 +184,7 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None):
return False return False
print(f"Generating CA certificate '{ca_name}' and key...") print(f"Generating CA certificate '{ca_name}' and key...")
# Path length constraint of 1 is set for the root CA to allow creating # Path length constraint of 1: allows one level of issuing CAs.
# one level of issuing CAs, but prevent a longer chain which is not
# supported by this script.
cmd = [ cmd = [
OPENSSL, "req", OPENSSL, "req",
"-x509", "-x509",
@@ -132,7 +203,10 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None):
return False return False
_rebuild_ca_bundle(ca_dir) _rebuild_ca_bundle(ca_dir)
_config.update({"aia_base_url": aia_base_url} if aia_base_url else {}) patch = {"name": ca_name, "created": _now()}
if ca_publish_base_url:
patch["ca_publish_base_url"] = ca_publish_base_url
_config.update(patch)
return True return True
issuing_ca_dir = os.path.join(ca_dir, issuing_ca) issuing_ca_dir = os.path.join(ca_dir, issuing_ca)
@@ -151,10 +225,10 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None):
"-addext", "basicConstraints=critical,CA:TRUE,pathlen:0", "-addext", "basicConstraints=critical,CA:TRUE,pathlen:0",
"-addext", "keyUsage=critical,keyCertSign,cRLSign", "-addext", "keyUsage=critical,keyCertSign,cRLSign",
] ]
if aia_base_url: if ca_publish_base_url:
req_cmd += [ req_cmd += [
"-addext", "-addext", f"authorityInfoAccess=caIssuers;URI:{ca_publish_base_url}/ca_cert.crt",
f"authorityInfoAccess=caIssuers;URI:{aia_base_url}/ca_cert.crt", "-addext", f"crlDistributionPoints=URI:{ca_publish_base_url}/crl.pem",
] ]
x509_cmd = [ x509_cmd = [
OPENSSL, "x509", OPENSSL, "x509",
@@ -170,7 +244,7 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None):
_err("Failed to generate issuing CA certificate and key.") _err("Failed to generate issuing CA certificate and key.")
return False return False
patch = {"aia_base_url": aia_base_url} if aia_base_url else {} patch = {"ca_publish_base_url": ca_publish_base_url} if ca_publish_base_url else {}
subs = _config.get("subordinates", []) subs = _config.get("subordinates", [])
if issuing_ca not in subs: if issuing_ca not in subs:
patch["subordinates"] = subs + [issuing_ca] patch["subordinates"] = subs + [issuing_ca]
@@ -179,29 +253,8 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None):
return True return True
_IP_RE = re.compile(r"^[0-9]{1,3}(\.[0-9]{1,3}){3}$")
_DNS_RE = re.compile(r"^[a-z0-9-]+(\.[a-z0-9-]+)*$")
def _is_ip(value):
return bool(_IP_RE.match(value))
def _is_dns(value):
return bool(_DNS_RE.match(value))
def _pipe(cmd1, cmd2):
p1 = subprocess.Popen(cmd1, stdout=subprocess.PIPE)
p2 = subprocess.Popen(cmd2, stdin=p1.stdout)
p1.stdout.close()
p2.communicate()
p1.wait()
return p1.returncode == 0 and p2.returncode == 0
def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None, def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None,
issuing_ca=None, days=365, aia_base_url=None): issuing_ca=None, days=365, ca_publish_base_url=None):
if issuing_ca == "ca": if issuing_ca == "ca":
_err("--issuing-ca cannot be 'ca' as it is reserved for the root CA.") _err("--issuing-ca cannot be 'ca' as it is reserved for the root CA.")
return False return False
@@ -224,7 +277,7 @@ def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None,
_err(f"Invalid subject name '{cert_subject_name}'. Must be a valid DNS name.") _err(f"Invalid subject name '{cert_subject_name}'. Must be a valid DNS name.")
return False return False
signing_dir = os.path.join(ca_dir, issuing_ca) if issuing_ca else ca_dir signing_dir = os.path.join(ca_dir, issuing_ca) if issuing_ca else ca_dir
ca_cert_path = os.path.join(signing_dir, "ca_cert.pem") ca_cert_path = os.path.join(signing_dir, "ca_cert.pem")
ca_key_path = os.path.join(signing_dir, "ca_key.pem") ca_key_path = os.path.join(signing_dir, "ca_key.pem")
if not os.path.isfile(ca_cert_path) or not os.path.isfile(ca_key_path): if not os.path.isfile(ca_cert_path) or not os.path.isfile(ca_key_path):
@@ -234,12 +287,15 @@ def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None,
) )
return False return False
if aia_base_url: aia_url = cdp_url = ""
aia_url = f"{aia_base_url}/{issuing_ca}/ca_cert.crt" if issuing_ca else f"{aia_base_url}/ca_cert.crt" if ca_publish_base_url:
else: if issuing_ca:
aia_url = "" aia_url = f"{ca_publish_base_url}/{issuing_ca}/ca_cert.crt"
cdp_url = f"{ca_publish_base_url}/{issuing_ca}/crl.pem"
else:
aia_url = f"{ca_publish_base_url}/ca_cert.crt"
cdp_url = f"{ca_publish_base_url}/crl.pem"
# "account" name from the subject: hostname part before the first dot
cert_name = cert_subject_name.split(".", 1)[0] cert_name = cert_subject_name.split(".", 1)[0]
san_entries = [f"DNS:{cert_subject_name}"] san_entries = [f"DNS:{cert_subject_name}"]
@@ -259,7 +315,7 @@ def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None,
print(f" - {san}") print(f" - {san}")
cert_out = os.path.join(cert_dir, f"{cert_name}_cert.pem") cert_out = os.path.join(cert_dir, f"{cert_name}_cert.pem")
key_out = os.path.join(cert_dir, f"{cert_name}_key.pem") key_out = os.path.join(cert_dir, f"{cert_name}_key.pem")
if not os.path.isfile(cert_out) or not os.path.isfile(key_out): if not os.path.isfile(cert_out) or not os.path.isfile(key_out):
print("Generating server certificate and key...") print("Generating server certificate and key...")
@@ -276,6 +332,8 @@ def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None,
] ]
if aia_url: if aia_url:
req_cmd += ["-addext", f"authorityInfoAccess=caIssuers;URI:{aia_url}"] req_cmd += ["-addext", f"authorityInfoAccess=caIssuers;URI:{aia_url}"]
if cdp_url:
req_cmd += ["-addext", f"crlDistributionPoints=URI:{cdp_url}"]
x509_cmd = [ x509_cmd = [
OPENSSL, "x509", OPENSSL, "x509",
"-req", "-req",
@@ -290,6 +348,13 @@ def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None,
_err("Failed to generate server certificate and key.") _err("Failed to generate server certificate and key.")
return False return False
_config.append_history(issuing_ca or "ca", {
"name": cert_subject_name,
"serial": _read_serial(cert_out),
"created": _now(),
"expires": _read_expiry(cert_out),
})
return True return True
@@ -298,13 +363,10 @@ def make_pfx(cert_path, ca_dir, issuing_ca=None, password=None, apple_openssl=Fa
_err("--issuing-ca cannot be 'ca' as it is reserved for the root CA.") _err("--issuing-ca cannot be 'ca' as it is reserved for the root CA.")
return False return False
cert_dir = os.path.dirname(cert_path) cert_dir = os.path.dirname(cert_path)
cert_basename = os.path.basename(cert_path) cert_basename = os.path.basename(cert_path)
if cert_basename.endswith("_cert.pem"): cert_name = cert_basename[:-len("_cert.pem")] if cert_basename.endswith("_cert.pem") else cert_basename
cert_name = cert_basename[: -len("_cert.pem")] key_path = os.path.join(cert_dir, f"{cert_name}_key.pem")
else:
cert_name = cert_basename
key_path = os.path.join(cert_dir, f"{cert_name}_key.pem")
if not cert_dir or not os.path.isdir(cert_dir): if not cert_dir or not os.path.isdir(cert_dir):
_err(f"Certificate directory {cert_dir} does not exist.") _err(f"Certificate directory {cert_dir} does not exist.")
@@ -347,7 +409,6 @@ def make_pfx(cert_path, ca_dir, issuing_ca=None, password=None, apple_openssl=Fa
with open(issuing_ca_cert_path, "rb") as f: with open(issuing_ca_cert_path, "rb") as f:
chain_bytes += f.read() chain_bytes += f.read()
import tempfile
chain_fd, chain_file = tempfile.mkstemp() chain_fd, chain_file = tempfile.mkstemp()
try: try:
with os.fdopen(chain_fd, "wb") as f: with os.fdopen(chain_fd, "wb") as f:
@@ -371,6 +432,82 @@ def make_pfx(cert_path, ca_dir, issuing_ca=None, password=None, apple_openssl=Fa
return True return True
def make_crl(ca_dir, issuing_ca=None, days=30):
signing_dir = os.path.join(ca_dir, issuing_ca) if issuing_ca else ca_dir
ca_cert = os.path.join(signing_dir, "ca_cert.pem")
ca_key = os.path.join(signing_dir, "ca_key.pem")
if not os.path.isfile(ca_cert) or not os.path.isfile(ca_key):
_err(f"CA certificate or key not found in {signing_dir}.")
return False
crl_path = os.path.join(signing_dir, "crl.pem")
history_key = issuing_ca or "ca"
revoked_entries = [
e for e in _config.get("history", {}).get(history_key, [])
if "revoked" in e and "expires" in e
]
with tempfile.TemporaryDirectory() as tmp:
index_txt = os.path.join(tmp, "index.txt")
crlnumber = os.path.join(tmp, "crlnumber")
cnf_path = os.path.join(tmp, "openssl.cnf")
with open(index_txt, "w") as f:
for e in revoked_entries:
expires_asn1 = _iso_to_asn1(e["expires"])
revoked_asn1 = _iso_to_asn1(e["revoked"])
f.write(f"R\t{expires_asn1}\t{revoked_asn1}\t{e['serial']}\tunknown\t/CN={e['name']}\n")
with open(crlnumber, "w") as f:
f.write("01\n")
with open(cnf_path, "w") as f:
f.write(
"[ ca ]\ndefault_ca = CA_default\n\n"
"[ CA_default ]\n"
f"database = {index_txt}\n"
f"crlnumber = {crlnumber}\n"
f"certificate = {ca_cert}\n"
f"private_key = {ca_key}\n"
f"default_crl_days = {days}\n"
"default_md = sha256\n\n"
"[ crl_ext ]\n"
"authorityKeyIdentifier = keyid:always\n"
)
if subprocess.run([OPENSSL, "ca", "-gencrl", "-config", cnf_path, "-out", crl_path]).returncode != 0:
_err("Failed to generate CRL.")
return False
print(f"CRL written to {crl_path}")
return True
def revoke_cert(cert_path, ca_dir, issuing_ca=None):
if not os.path.isfile(cert_path):
_err(f"Certificate not found: {cert_path}")
return False
signing_dir = os.path.join(ca_dir, issuing_ca) if issuing_ca else ca_dir
if not os.path.isdir(signing_dir):
_err(f"CA directory not found: {signing_dir}")
return False
ca_key = issuing_ca or "ca"
serial = _read_serial(cert_path)
result = _config.revoke_in_history(ca_key, serial, _now())
if result is None:
_err(f"Certificate with serial {serial} not found in history for CA '{ca_key}'.")
return False
if result is False:
print(f"Certificate {cert_path} (serial {serial}) is already revoked.")
return True
print(f"Certificate {cert_path} (serial {serial}) marked as revoked.")
return True
def _build_parser(): def _build_parser():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Simple CA for creating and managing test certificates." description="Simple CA for creating and managing test certificates."
@@ -380,7 +517,8 @@ def _build_parser():
p_ca = sub.add_parser("make-ca", help="Create a root or issuing CA.") p_ca = sub.add_parser("make-ca", help="Create a root or issuing CA.")
p_ca.add_argument("--days", type=int, default=None, help="Validity period in days (default: 3650)") 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("--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-publish-base-url", default=None,
help="Base URL for AIA and CRL distribution point extensions")
p_ca.add_argument("--ca-dir", help="Directory to store the CA files") p_ca.add_argument("--ca-dir", help="Directory to store the CA files")
p_ca.add_argument("--openssl", default=None, metavar="PATH", p_ca.add_argument("--openssl", default=None, metavar="PATH",
help=f"Path to the openssl binary (default: {OPENSSL})") help=f"Path to the openssl binary (default: {OPENSSL})")
@@ -404,6 +542,16 @@ def _build_parser():
help="Use Apple's bundled /usr/bin/openssl for PKCS12 generation") help="Use Apple's bundled /usr/bin/openssl for PKCS12 generation")
p_pfx.add_argument("path", help="Path to the certificate file") p_pfx.add_argument("path", help="Path to the certificate file")
p_crl = sub.add_parser("make-crl", help="Generate a CRL for a CA.")
p_crl.add_argument("--ca-dir", help="Directory of the CA")
p_crl.add_argument("--issuing-ca", default=None, help="Generate CRL for this issuing CA")
p_crl.add_argument("--days", type=int, default=None, help="CRL validity in days (default: 30)")
p_rev = sub.add_parser("revoke-cert", help="Revoke a certificate.")
p_rev.add_argument("--ca-dir", help="Directory of the CA")
p_rev.add_argument("--issuing-ca", default=None, help="Issuing CA that signed the certificate")
p_rev.add_argument("cert_path", help="Path to the certificate file to revoke")
return parser return parser
@@ -416,9 +564,9 @@ def main(argv=None):
_config = Config(ca_dir) _config = Config(ca_dir)
OPENSSL = getattr(args, "openssl", None) or _config.get("openssl", OPENSSL) OPENSSL = getattr(args, "openssl", None) or _config.get("openssl", OPENSSL)
issuing_ca = args.issuing_ca or _config.get("issuing_ca") issuing_ca = args.issuing_ca or _config.get("issuing_ca")
aia_base_url = getattr(args, "aia_base_url", None) or _config.get("aia_base_url") ca_publish_base_url = getattr(args, "ca_publish_base_url", None) or _config.get("ca_publish_base_url")
days_cfg = _config.get("days", {}) days_cfg = _config.get("days", {})
@@ -428,7 +576,7 @@ def main(argv=None):
ca_dir, args.ca_name, ca_dir, args.ca_name,
days=days, days=days,
issuing_ca=issuing_ca, issuing_ca=issuing_ca,
aia_base_url=aia_base_url, ca_publish_base_url=ca_publish_base_url,
) )
elif args.command == "make-cert": elif args.command == "make-cert":
days = args.days or days_cfg.get("cert", 365) days = args.days or days_cfg.get("cert", 365)
@@ -438,7 +586,7 @@ def main(argv=None):
ca_dir=ca_dir, ca_dir=ca_dir,
issuing_ca=issuing_ca, issuing_ca=issuing_ca,
days=days, days=days,
aia_base_url=aia_base_url, ca_publish_base_url=ca_publish_base_url,
) )
elif args.command == "make-pfx": elif args.command == "make-pfx":
ok = make_pfx( ok = make_pfx(
@@ -447,6 +595,11 @@ def main(argv=None):
password=args.password, password=args.password,
apple_openssl=args.apple_openssl, apple_openssl=args.apple_openssl,
) )
elif args.command == "make-crl":
days = args.days or days_cfg.get("crl", 30)
ok = make_crl(ca_dir, issuing_ca=issuing_ca, days=days)
elif args.command == "revoke-cert":
ok = revoke_cert(args.cert_path, ca_dir, issuing_ca=issuing_ca)
else: else:
parser.error(f"Unknown command: {args.command}") parser.error(f"Unknown command: {args.command}")
ok = False ok = False