Remove simple-ca module and its associated files
- 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:
@@ -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 (~5–6 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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user