Refactor simple-ca: Remove JSON config and streamline AIA URL handling
- Removed the JSON configuration structure and related functions. - Introduced plain text file for AIA base URL management. - Updated CA and certificate creation functions to directly read/write AIA URL. - Simplified CA bundle rebuilding logic by directly reading subdirectories. - Enhanced test coverage for CA and certificate creation, including PFX generation. - Adjusted test cases to reflect changes in directory structure and file handling.
This commit is contained in:
@@ -5,6 +5,7 @@ on:
|
|||||||
- 'simple-ca.py'
|
- 'simple-ca.py'
|
||||||
- 'run-tests.sh'
|
- 'run-tests.sh'
|
||||||
- 'test_simple_ca.py'
|
- 'test_simple_ca.py'
|
||||||
|
- 'src/simple-ca/**'
|
||||||
- '.gitea/workflows/test.yaml'
|
- '.gitea/workflows/test.yaml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -33,3 +34,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Run shell tests
|
- name: Run shell tests
|
||||||
run: bash run-tests.sh
|
run: bash run-tests.sh
|
||||||
|
|
||||||
|
test-go:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run Go tests
|
||||||
|
run: go test -v ./...
|
||||||
|
working-directory: src/simple-ca
|
||||||
|
|||||||
+15
-15
@@ -34,11 +34,10 @@ TEST_DIR="$(mktemp -d)"
|
|||||||
trap 'rm -rf "$TEST_DIR"' EXIT
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
|
|
||||||
CA_DIR="$TEST_DIR/ca"
|
CA_DIR="$TEST_DIR/ca"
|
||||||
CERT_DIR="$TEST_DIR/certs"
|
|
||||||
|
|
||||||
reset_dirs() {
|
reset_dirs() {
|
||||||
rm -rf "$CA_DIR" "$CERT_DIR"
|
rm -rf "$CA_DIR"
|
||||||
mkdir -p "$CA_DIR" "$CERT_DIR"
|
mkdir -p "$CA_DIR"
|
||||||
SIMPLE_CA_DIR=""
|
SIMPLE_CA_DIR=""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,23 +51,23 @@ verify_cert() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Standalone CA
|
# Standalone CA — certs issued by root CA go into CA_DIR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "--- [shell] Standalone CA ---"
|
echo "--- [shell] Standalone CA ---"
|
||||||
reset_dirs
|
reset_dirs
|
||||||
make_ca --ca-dir "$CA_DIR" "Test CA" 2>/dev/null
|
make_ca --ca-dir "$CA_DIR" "Test CA" 2>/dev/null
|
||||||
[[ -f "$CA_DIR/ca_cert.pem" ]] || { echo "ERROR: ca_cert.pem not created" >&2; exit 1; }
|
[[ -f "$CA_DIR/ca_cert.pem" ]] || { echo "ERROR: ca_cert.pem not created" >&2; exit 1; }
|
||||||
[[ -f "$CA_DIR/ca_bundle.pem" ]] || { echo "ERROR: ca_bundle.pem not created" >&2; exit 1; }
|
[[ -f "$CA_DIR/ca_bundle.pem" ]] || { echo "ERROR: ca_bundle.pem not created" >&2; exit 1; }
|
||||||
verify_cert "$CA_DIR/ca_cert.pem"
|
verify_cert "$CA_DIR/ca_cert.pem"
|
||||||
|
|
||||||
make_cert --cert-dir "$CERT_DIR" "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
make_cert "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
||||||
[[ -f "$CERT_DIR/test_cert.pem" ]] || { echo "ERROR: test_cert.pem not created" >&2; exit 1; }
|
[[ -f "$CA_DIR/test_cert.pem" ]] || { echo "ERROR: test_cert.pem not created in CA_DIR" >&2; exit 1; }
|
||||||
verify_cert "$CERT_DIR/test_cert.pem"
|
verify_cert "$CA_DIR/test_cert.pem"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Two-level CA
|
# Two-level CA — issuing CA and its certs go into CA_DIR/issuing_ca/
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
echo
|
echo
|
||||||
@@ -81,12 +80,13 @@ make_ca --issuing-ca "issuing_ca" "Issuing CA" 2>/dev/null
|
|||||||
[[ -f "$CA_DIR/issuing_ca/ca_cert.pem" ]] || { echo "ERROR: issuing_ca/ca_cert.pem not created" >&2; exit 1; }
|
[[ -f "$CA_DIR/issuing_ca/ca_cert.pem" ]] || { echo "ERROR: issuing_ca/ca_cert.pem not created" >&2; exit 1; }
|
||||||
verify_cert "$CA_DIR/issuing_ca/ca_cert.pem"
|
verify_cert "$CA_DIR/issuing_ca/ca_cert.pem"
|
||||||
|
|
||||||
make_cert --cert-dir "$CERT_DIR" --issuing-ca "issuing_ca" "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
make_cert --issuing-ca "issuing_ca" "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
||||||
verify_cert "$CERT_DIR/test_cert.pem"
|
[[ -f "$CA_DIR/issuing_ca/test_cert.pem" ]] || { echo "ERROR: issuing_ca/test_cert.pem not created" >&2; exit 1; }
|
||||||
|
verify_cert "$CA_DIR/issuing_ca/test_cert.pem"
|
||||||
|
|
||||||
make_pfx --issuing-ca "issuing_ca" --password "s3cr3t" "$CERT_DIR/test_cert.pem" 2>/dev/null
|
make_pfx --issuing-ca "issuing_ca" --password "s3cr3t" "$CA_DIR/issuing_ca/test_cert.pem" 2>/dev/null
|
||||||
[[ -f "$CERT_DIR/test.pfx" ]] || { echo "ERROR: test.pfx not created" >&2; exit 1; }
|
[[ -f "$CA_DIR/issuing_ca/test.pfx" ]] || { echo "ERROR: issuing_ca/test.pfx not created" >&2; exit 1; }
|
||||||
openssl pkcs12 -in "$CERT_DIR/test.pfx" -noout -info -password pass:"s3cr3t" 2>/dev/null \
|
openssl pkcs12 -in "$CA_DIR/issuing_ca/test.pfx" -noout -info -password pass:"s3cr3t" 2>/dev/null \
|
||||||
|| { echo "ERROR: PFX verification failed" >&2; exit 1; }
|
|| { echo "ERROR: PFX verification failed" >&2; exit 1; }
|
||||||
echo "PFX: OK"
|
echo "PFX: OK"
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -253,18 +253,12 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, ca_publish_base_url=Non
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None,
|
def make_cert(cert_subject_name, sans=None, ca_dir=None, cert_dir=None,
|
||||||
issuing_ca=None, days=365, ca_publish_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
|
||||||
|
|
||||||
ca_dir = ca_dir or cert_dir
|
|
||||||
|
|
||||||
if not cert_dir or not os.path.isdir(cert_dir):
|
|
||||||
_err(f"Certificate directory {cert_dir} does not exist.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not ca_dir or not os.path.isdir(ca_dir):
|
if not ca_dir or not os.path.isdir(ca_dir):
|
||||||
_err(f"CA directory {ca_dir} does not exist.")
|
_err(f"CA directory {ca_dir} does not exist.")
|
||||||
return False
|
return False
|
||||||
@@ -278,6 +272,11 @@ def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None,
|
|||||||
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
|
||||||
|
cert_dir = cert_dir or signing_dir
|
||||||
|
|
||||||
|
if not os.path.isdir(cert_dir):
|
||||||
|
_err(f"Certificate directory {cert_dir} does not exist.")
|
||||||
|
return False
|
||||||
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):
|
||||||
@@ -581,9 +580,10 @@ def main(argv=None):
|
|||||||
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)
|
||||||
ok = make_cert(
|
ok = make_cert(
|
||||||
args.cert_dir, args.subject_name,
|
args.subject_name,
|
||||||
sans=args.sans,
|
sans=args.sans,
|
||||||
ca_dir=ca_dir,
|
ca_dir=ca_dir,
|
||||||
|
cert_dir=getattr(args, "cert_dir", None),
|
||||||
issuing_ca=issuing_ca,
|
issuing_ca=issuing_ca,
|
||||||
days=days,
|
days=days,
|
||||||
ca_publish_base_url=ca_publish_base_url,
|
ca_publish_base_url=ca_publish_base_url,
|
||||||
|
|||||||
+129
-40
@@ -26,12 +26,15 @@
|
|||||||
# before sourcing this file, or overridden per-call with --ca-dir. Once set by any
|
# before sourcing this file, or overridden per-call with --ca-dir. Once set by any
|
||||||
# call, subsequent calls in the same session inherit it.
|
# call, subsequent calls in the same session inherit it.
|
||||||
#
|
#
|
||||||
# Issuing CAs live in subdirectories of SIMPLE_CA_DIR:
|
# Directory layout:
|
||||||
# $SIMPLE_CA_DIR/ca_cert.pem — root CA certificate
|
# $SIMPLE_CA_DIR/ca_cert.pem — root CA certificate
|
||||||
# $SIMPLE_CA_DIR/ca_key.pem — root CA private key
|
# $SIMPLE_CA_DIR/ca_key.pem — root CA private key
|
||||||
|
# $SIMPLE_CA_DIR/{name}_cert.pem — certificates issued by the root CA
|
||||||
# $SIMPLE_CA_DIR/{issuing_ca}/ca_cert.pem — issuing CA certificate
|
# $SIMPLE_CA_DIR/{issuing_ca}/ca_cert.pem — issuing CA certificate
|
||||||
# $SIMPLE_CA_DIR/{issuing_ca}/ca_key.pem — issuing CA private key
|
# $SIMPLE_CA_DIR/{issuing_ca}/ca_key.pem — issuing CA private key
|
||||||
|
# $SIMPLE_CA_DIR/{issuing_ca}/{name}_cert.pem — certificates issued by that issuing CA
|
||||||
#
|
#
|
||||||
|
# Certificates are always written to the directory of the CA that signs them.
|
||||||
# Any subdirectory containing ca_cert.pem is treated as an issuing CA.
|
# Any subdirectory containing ca_cert.pem is treated as an issuing CA.
|
||||||
|
|
||||||
SIMPLE_CA_DIR="${SIMPLE_CA_DIR:-}"
|
SIMPLE_CA_DIR="${SIMPLE_CA_DIR:-}"
|
||||||
@@ -65,17 +68,32 @@ function make_ca() {
|
|||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--days)
|
--days)
|
||||||
[[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]] && { echo "ERROR: --days requires a positive integer." >&2; return 1; }
|
if [[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "ERROR: --days requires a positive integer." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
CA_DAYS="$2"; shift 2 ;;
|
CA_DAYS="$2"; shift 2 ;;
|
||||||
--issuing-ca)
|
--issuing-ca)
|
||||||
[[ -z "$2" ]] && { echo "ERROR: --issuing-ca requires a value." >&2; return 1; }
|
if [[ -z "$2" ]]; then
|
||||||
[[ "$2" == "ca" ]] && { echo "ERROR: --issuing-ca cannot be 'ca'." >&2; return 1; }
|
echo "ERROR: --issuing-ca requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$2" == "ca" ]]; then
|
||||||
|
echo "ERROR: --issuing-ca cannot be 'ca'." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
ISSUING_CA="$2"; shift 2 ;;
|
ISSUING_CA="$2"; shift 2 ;;
|
||||||
--aia-base-url)
|
--aia-base-url)
|
||||||
[[ -z "$2" ]] && { echo "ERROR: --aia-base-url requires a value." >&2; return 1; }
|
if [[ -z "$2" ]]; then
|
||||||
|
echo "ERROR: --aia-base-url requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
AIA_BASE_URL="$2"; shift 2 ;;
|
AIA_BASE_URL="$2"; shift 2 ;;
|
||||||
--ca-dir)
|
--ca-dir)
|
||||||
[[ -z "$2" ]] && { echo "ERROR: --ca-dir requires a value." >&2; return 1; }
|
if [[ -z "$2" ]]; then
|
||||||
|
echo "ERROR: --ca-dir requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
SIMPLE_CA_DIR="$2"; shift 2 ;;
|
SIMPLE_CA_DIR="$2"; shift 2 ;;
|
||||||
*) break ;;
|
*) break ;;
|
||||||
esac
|
esac
|
||||||
@@ -84,10 +102,14 @@ function make_ca() {
|
|||||||
local CA_NAME="$1"
|
local CA_NAME="$1"
|
||||||
_require_ca_dir || return 1
|
_require_ca_dir || return 1
|
||||||
|
|
||||||
[[ -z "$CA_NAME" ]] && { echo "ERROR: CA name is required." >&2; return 1; }
|
if [[ -z "$CA_NAME" ]]; then
|
||||||
|
echo "ERROR: CA name is required." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
[[ -z "$AIA_BASE_URL" && -f "$SIMPLE_CA_DIR/aia_base_url.txt" ]] \
|
if [[ -z "$AIA_BASE_URL" && -f "$SIMPLE_CA_DIR/aia_base_url.txt" ]]; then
|
||||||
&& AIA_BASE_URL="$(cat "$SIMPLE_CA_DIR/aia_base_url.txt")"
|
AIA_BASE_URL="$(cat "$SIMPLE_CA_DIR/aia_base_url.txt")"
|
||||||
|
fi
|
||||||
|
|
||||||
local ROOT_CA_CERT="$SIMPLE_CA_DIR/ca_cert.pem"
|
local ROOT_CA_CERT="$SIMPLE_CA_DIR/ca_cert.pem"
|
||||||
local ROOT_CA_KEY="$SIMPLE_CA_DIR/ca_key.pem"
|
local ROOT_CA_KEY="$SIMPLE_CA_DIR/ca_key.pem"
|
||||||
@@ -114,7 +136,9 @@ function make_ca() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
_rebuild_ca_bundle
|
_rebuild_ca_bundle
|
||||||
[[ -n "$AIA_BASE_URL" ]] && echo "$AIA_BASE_URL" > "$SIMPLE_CA_DIR/aia_base_url.txt"
|
if [[ -n "$AIA_BASE_URL" ]]; then
|
||||||
|
echo "$AIA_BASE_URL" > "$SIMPLE_CA_DIR/aia_base_url.txt"
|
||||||
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -167,38 +191,62 @@ function make_cert() {
|
|||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--ca-dir)
|
--ca-dir)
|
||||||
[[ -z "$2" ]] && { echo "ERROR: --ca-dir requires a value." >&2; return 1; }
|
if [[ -z "$2" ]]; then
|
||||||
|
echo "ERROR: --ca-dir requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
SIMPLE_CA_DIR="$2"; shift 2 ;;
|
SIMPLE_CA_DIR="$2"; shift 2 ;;
|
||||||
--cert-dir)
|
--cert-dir)
|
||||||
[[ -z "$2" ]] && { echo "ERROR: --cert-dir requires a value." >&2; return 1; }
|
if [[ -z "$2" ]]; then
|
||||||
|
echo "ERROR: --cert-dir requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
CERT_DIR="$2"; shift 2 ;;
|
CERT_DIR="$2"; shift 2 ;;
|
||||||
--issuing-ca)
|
--issuing-ca)
|
||||||
[[ -z "$2" ]] && { echo "ERROR: --issuing-ca requires a value." >&2; return 1; }
|
if [[ -z "$2" ]]; then
|
||||||
[[ "$2" == "ca" ]] && { echo "ERROR: --issuing-ca cannot be 'ca'." >&2; return 1; }
|
echo "ERROR: --issuing-ca requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$2" == "ca" ]]; then
|
||||||
|
echo "ERROR: --issuing-ca cannot be 'ca'." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
ISSUING_CA="$2"; shift 2 ;;
|
ISSUING_CA="$2"; shift 2 ;;
|
||||||
--days)
|
--days)
|
||||||
[[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]] && { echo "ERROR: --days requires a positive integer." >&2; return 1; }
|
if [[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "ERROR: --days requires a positive integer." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
CERT_DAYS="$2"; shift 2 ;;
|
CERT_DAYS="$2"; shift 2 ;;
|
||||||
*) break ;;
|
*) break ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
local CERT_SUBJECT_NAME="$1"
|
local CERT_SUBJECT_NAME="$1"
|
||||||
[[ $# -gt 0 ]] && shift
|
if [[ $# -gt 0 ]]; then
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
_require_ca_dir || return 1
|
_require_ca_dir || return 1
|
||||||
|
|
||||||
[[ -z "$CERT_DIR" ]] && { echo "ERROR: --cert-dir is required." >&2; return 1; }
|
if [[ -z "$CERT_SUBJECT_NAME" ]]; then
|
||||||
[[ ! -d "$CERT_DIR" ]] && { echo "ERROR: Certificate directory '$CERT_DIR' does not exist." >&2; return 1; }
|
echo "ERROR: Subject name is required." >&2
|
||||||
[[ -z "$CERT_SUBJECT_NAME" ]] && { echo "ERROR: Subject name is required." >&2; return 1; }
|
return 1
|
||||||
_is_dns "$CERT_SUBJECT_NAME" || { echo "ERROR: Invalid subject name '$CERT_SUBJECT_NAME'. Must be a valid DNS name." >&2; return 1; }
|
fi
|
||||||
|
if ! _is_dns "$CERT_SUBJECT_NAME"; then
|
||||||
|
echo "ERROR: Invalid subject name '$CERT_SUBJECT_NAME'. Must be a valid DNS name." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
local SIGNING_DIR="$SIMPLE_CA_DIR${ISSUING_CA:+/$ISSUING_CA}"
|
local SIGNING_DIR="$SIMPLE_CA_DIR${ISSUING_CA:+/$ISSUING_CA}"
|
||||||
local SIGNING_CERT="$SIGNING_DIR/ca_cert.pem"
|
local SIGNING_CERT="$SIGNING_DIR/ca_cert.pem"
|
||||||
local SIGNING_KEY="$SIGNING_DIR/ca_key.pem"
|
local SIGNING_KEY="$SIGNING_DIR/ca_key.pem"
|
||||||
|
CERT_DIR="${CERT_DIR:-$SIGNING_DIR}"
|
||||||
|
|
||||||
[[ ! -f "$SIGNING_CERT" || ! -f "$SIGNING_KEY" ]] \
|
if [[ ! -f "$SIGNING_CERT" || ! -f "$SIGNING_KEY" ]]; then
|
||||||
&& { echo "ERROR: Signing CA certificate and key not found in $SIGNING_DIR." >&2; return 1; }
|
echo "ERROR: Signing CA certificate and key not found in $SIGNING_DIR." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
local AIA_URL=""
|
local AIA_URL=""
|
||||||
if [[ -f "$SIMPLE_CA_DIR/aia_base_url.txt" ]]; then
|
if [[ -f "$SIMPLE_CA_DIR/aia_base_url.txt" ]]; then
|
||||||
@@ -211,9 +259,13 @@ function make_cert() {
|
|||||||
local SANS=("DNS:${CERT_SUBJECT_NAME}")
|
local SANS=("DNS:${CERT_SUBJECT_NAME}")
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
if _is_ip "$1"; then SANS+=("IP:$1")
|
if _is_ip "$1"; then
|
||||||
elif _is_dns "$1"; then SANS+=("DNS:$1")
|
SANS+=("IP:$1")
|
||||||
else { echo "ERROR: Invalid SAN entry '$1'." >&2; return 1; }
|
elif _is_dns "$1"; then
|
||||||
|
SANS+=("DNS:$1")
|
||||||
|
else
|
||||||
|
echo "ERROR: Invalid SAN entry '$1'." >&2
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
@@ -255,19 +307,34 @@ function make_cert() {
|
|||||||
function make_pfx() {
|
function make_pfx() {
|
||||||
local ISSUING_CA=""
|
local ISSUING_CA=""
|
||||||
local PFX_PASSWORD=""
|
local PFX_PASSWORD=""
|
||||||
|
local APPLE_OPENSSL=0
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--ca-dir)
|
--ca-dir)
|
||||||
[[ -z "$2" ]] && { echo "ERROR: --ca-dir requires a value." >&2; return 1; }
|
if [[ -z "$2" ]]; then
|
||||||
|
echo "ERROR: --ca-dir requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
SIMPLE_CA_DIR="$2"; shift 2 ;;
|
SIMPLE_CA_DIR="$2"; shift 2 ;;
|
||||||
--issuing-ca)
|
--issuing-ca)
|
||||||
[[ -z "$2" ]] && { echo "ERROR: --issuing-ca requires a value." >&2; return 1; }
|
if [[ -z "$2" ]]; then
|
||||||
[[ "$2" == "ca" ]] && { echo "ERROR: --issuing-ca cannot be 'ca'." >&2; return 1; }
|
echo "ERROR: --issuing-ca requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$2" == "ca" ]]; then
|
||||||
|
echo "ERROR: --issuing-ca cannot be 'ca'." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
ISSUING_CA="$2"; shift 2 ;;
|
ISSUING_CA="$2"; shift 2 ;;
|
||||||
--password)
|
--password)
|
||||||
[[ -z "$2" ]] && { echo "ERROR: --password requires a value." >&2; return 1; }
|
if [[ -z "$2" ]]; then
|
||||||
|
echo "ERROR: --password requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
PFX_PASSWORD="$2"; shift 2 ;;
|
PFX_PASSWORD="$2"; shift 2 ;;
|
||||||
|
--apple-openssl)
|
||||||
|
APPLE_OPENSSL=1; shift ;;
|
||||||
*) break ;;
|
*) break ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -275,24 +342,44 @@ function make_pfx() {
|
|||||||
local CERT_PATH="$1"
|
local CERT_PATH="$1"
|
||||||
_require_ca_dir || return 1
|
_require_ca_dir || return 1
|
||||||
|
|
||||||
[[ -z "$CERT_PATH" ]] && { echo "ERROR: Certificate path is required." >&2; return 1; }
|
if [[ -z "$CERT_PATH" ]]; then
|
||||||
|
echo "ERROR: Certificate path is required." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
local CERT_DIR CERT_NAME KEY_PATH
|
local CERT_DIR CERT_NAME KEY_PATH
|
||||||
CERT_DIR="$(dirname "$CERT_PATH")"
|
CERT_DIR="$(dirname "$CERT_PATH")"
|
||||||
CERT_NAME="$(basename "$CERT_PATH" _cert.pem)"
|
CERT_NAME="$(basename "$CERT_PATH" _cert.pem)"
|
||||||
KEY_PATH="$CERT_DIR/${CERT_NAME}_key.pem"
|
KEY_PATH="$CERT_DIR/${CERT_NAME}_key.pem"
|
||||||
|
|
||||||
[[ ! -d "$CERT_DIR" ]] && { echo "ERROR: Certificate directory '$CERT_DIR' does not exist." >&2; return 1; }
|
if [[ ! -d "$CERT_DIR" ]]; then
|
||||||
[[ ! -f "$CERT_PATH" || ! -f "$KEY_PATH" ]] && { echo "ERROR: Server certificate or key not found." >&2; return 1; }
|
echo "ERROR: Certificate directory '$CERT_DIR' does not exist." >&2
|
||||||
[[ ! -f "$SIMPLE_CA_DIR/ca_cert.pem" ]] && { echo "ERROR: Root CA certificate not found in $SIMPLE_CA_DIR." >&2; return 1; }
|
return 1
|
||||||
|
fi
|
||||||
[[ -n "$ISSUING_CA" && ! -f "$SIMPLE_CA_DIR/$ISSUING_CA/ca_cert.pem" ]] \
|
if [[ ! -f "$CERT_PATH" || ! -f "$KEY_PATH" ]]; then
|
||||||
&& { echo "ERROR: Issuing CA certificate not found in $SIMPLE_CA_DIR/$ISSUING_CA." >&2; return 1; }
|
echo "ERROR: Server certificate or key not found." >&2
|
||||||
|
return 1
|
||||||
[[ -f "$CERT_DIR/${CERT_NAME}.pfx" ]] && { echo "PKCS#12 (PFX) file already exists, aborting generation." >&2; return 1; }
|
fi
|
||||||
|
if [[ ! -f "$SIMPLE_CA_DIR/ca_cert.pem" ]]; then
|
||||||
|
echo "ERROR: Root CA certificate not found in $SIMPLE_CA_DIR." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -n "$ISSUING_CA" && ! -f "$SIMPLE_CA_DIR/$ISSUING_CA/ca_cert.pem" ]]; then
|
||||||
|
echo "ERROR: Issuing CA certificate not found in $SIMPLE_CA_DIR/$ISSUING_CA." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -f "$CERT_DIR/${CERT_NAME}.pfx" ]]; then
|
||||||
|
echo "PKCS#12 (PFX) file already exists, aborting generation." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
PFX_PASSWORD="${PFX_PASSWORD:-changeit}"
|
PFX_PASSWORD="${PFX_PASSWORD:-changeit}"
|
||||||
|
|
||||||
|
local OPENSSL_BIN="openssl"
|
||||||
|
if [[ "$APPLE_OPENSSL" -eq 1 ]]; then
|
||||||
|
OPENSSL_BIN="/usr/bin/openssl"
|
||||||
|
fi
|
||||||
|
|
||||||
echo -n "Generating PKCS#12 (PFX) file..."
|
echo -n "Generating PKCS#12 (PFX) file..."
|
||||||
|
|
||||||
local CHAIN_FILE
|
local CHAIN_FILE
|
||||||
@@ -300,9 +387,11 @@ function make_pfx() {
|
|||||||
trap "rm -f '$CHAIN_FILE'" EXIT QUIT KILL INT HUP
|
trap "rm -f '$CHAIN_FILE'" EXIT QUIT KILL INT HUP
|
||||||
|
|
||||||
cat "$SIMPLE_CA_DIR/ca_cert.pem" > "$CHAIN_FILE"
|
cat "$SIMPLE_CA_DIR/ca_cert.pem" > "$CHAIN_FILE"
|
||||||
[[ -n "$ISSUING_CA" ]] && cat "$SIMPLE_CA_DIR/$ISSUING_CA/ca_cert.pem" >> "$CHAIN_FILE"
|
if [[ -n "$ISSUING_CA" ]]; then
|
||||||
|
cat "$SIMPLE_CA_DIR/$ISSUING_CA/ca_cert.pem" >> "$CHAIN_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
if ! openssl pkcs12 \
|
if ! "$OPENSSL_BIN" pkcs12 \
|
||||||
-export \
|
-export \
|
||||||
-out "$CERT_DIR/${CERT_NAME}.pfx" \
|
-out "$CERT_DIR/${CERT_NAME}.pfx" \
|
||||||
-inkey "$KEY_PATH" \
|
-inkey "$KEY_PATH" \
|
||||||
|
|||||||
+114
-219
@@ -27,7 +27,6 @@ import (
|
|||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -37,7 +36,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -113,85 +111,27 @@ func loadKey(path string) (*rsa.PrivateKey, error) {
|
|||||||
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Config -----------------------------------------------------------------
|
// ---- AIA base URL -----------------------------------------------------------
|
||||||
|
|
||||||
type daysConfig struct {
|
func readAIABaseURL(caDir string) string {
|
||||||
CA int `json:"ca,omitempty"`
|
b, err := os.ReadFile(filepath.Join(caDir, "aia_base_url.txt"))
|
||||||
Cert int `json:"cert,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type configData struct {
|
|
||||||
OpenSSL string `json:"openssl,omitempty"`
|
|
||||||
AIABaseURL string `json:"aia_base_url,omitempty"`
|
|
||||||
IssuingCA string `json:"issuing_ca,omitempty"`
|
|
||||||
Days *daysConfig `json:"days,omitempty"`
|
|
||||||
Subordinates []string `json:"subordinates,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
path string
|
|
||||||
data configData
|
|
||||||
// isNew is true when the file did not exist at load time,
|
|
||||||
// so the first mutation always creates it.
|
|
||||||
isNew bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig(caDir string) *Config {
|
|
||||||
cfg := &Config{path: filepath.Join(caDir, "simple-ca.json")}
|
|
||||||
b, err := os.ReadFile(cfg.path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg.isNew = true
|
return ""
|
||||||
return cfg
|
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(b, &cfg.data); err != nil {
|
return strings.TrimSpace(string(b))
|
||||||
fmt.Fprintf(os.Stderr, "WARNING: could not read %s: %v\n", cfg.path, err)
|
|
||||||
cfg.isNew = true
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) save() error {
|
func writeAIABaseURL(caDir, url string) error {
|
||||||
b, err := json.MarshalIndent(c.data, "", " ")
|
return os.WriteFile(filepath.Join(caDir, "aia_base_url.txt"), []byte(url+"\n"), 0o644)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.isNew = false
|
|
||||||
return os.WriteFile(c.path, append(b, '\n'), 0o644)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure writes the file if it did not exist at load time.
|
|
||||||
func (c *Config) ensure() error {
|
|
||||||
if c.isNew {
|
|
||||||
return c.save()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setAIABaseURL updates the stored URL (if changed) and persists.
|
|
||||||
func (c *Config) setAIABaseURL(url string) error {
|
|
||||||
if url != "" && c.data.AIABaseURL != url {
|
|
||||||
c.data.AIABaseURL = url
|
|
||||||
return c.save()
|
|
||||||
}
|
|
||||||
return c.ensure()
|
|
||||||
}
|
|
||||||
|
|
||||||
// addSubordinate registers name in the subordinates list (if absent) and persists.
|
|
||||||
func (c *Config) addSubordinate(name string) error {
|
|
||||||
for _, s := range c.data.Subordinates {
|
|
||||||
if s == name {
|
|
||||||
return c.ensure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.data.Subordinates = append(c.data.Subordinates, name)
|
|
||||||
sort.Strings(c.data.Subordinates)
|
|
||||||
return c.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- CA bundle --------------------------------------------------------------
|
// ---- CA bundle --------------------------------------------------------------
|
||||||
|
|
||||||
func rebuildCABundle(caDir string, cfg *Config) error {
|
// rebuildCABundle writes ca_bundle.pem containing the root CA cert followed by
|
||||||
|
// all issuing CA certs found in immediate subdirectories.
|
||||||
|
func rebuildCABundle(caDir string) error {
|
||||||
var bundle []byte
|
var bundle []byte
|
||||||
|
|
||||||
rootPath := filepath.Join(caDir, "ca_cert.pem")
|
rootPath := filepath.Join(caDir, "ca_cert.pem")
|
||||||
if fileExists(rootPath) {
|
if fileExists(rootPath) {
|
||||||
data, err := os.ReadFile(rootPath)
|
data, err := os.ReadFile(rootPath)
|
||||||
@@ -200,10 +140,13 @@ func rebuildCABundle(caDir string, cfg *Config) error {
|
|||||||
}
|
}
|
||||||
bundle = append(bundle, data...)
|
bundle = append(bundle, data...)
|
||||||
}
|
}
|
||||||
subs := append([]string(nil), cfg.data.Subordinates...)
|
|
||||||
sort.Strings(subs)
|
entries, _ := os.ReadDir(caDir)
|
||||||
for _, name := range subs {
|
for _, e := range entries {
|
||||||
subCert := filepath.Join(caDir, name, "ca_cert.pem")
|
if !e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subCert := filepath.Join(caDir, e.Name(), "ca_cert.pem")
|
||||||
if !fileExists(subCert) {
|
if !fileExists(subCert) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -213,25 +156,25 @@ func rebuildCABundle(caDir string, cfg *Config) error {
|
|||||||
}
|
}
|
||||||
bundle = append(bundle, data...)
|
bundle = append(bundle, data...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(filepath.Join(caDir, "ca_bundle.pem"), bundle, 0o644)
|
return os.WriteFile(filepath.Join(caDir, "ca_bundle.pem"), bundle, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- makeCA -----------------------------------------------------------------
|
// ---- makeCA -----------------------------------------------------------------
|
||||||
|
|
||||||
func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string, cfg *Config) error {
|
func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string) error {
|
||||||
if issuingCA == "ca" {
|
if issuingCA == "ca" {
|
||||||
return errors.New("--issuing-ca cannot be 'ca' as it is reserved for the root CA")
|
return errors.New("--issuing-ca cannot be 'ca'")
|
||||||
}
|
}
|
||||||
if caDir == "" || !dirExists(caDir) {
|
if !dirExists(caDir) {
|
||||||
return fmt.Errorf("certificate directory %s does not exist", caDir)
|
return fmt.Errorf("CA directory %s does not exist", caDir)
|
||||||
}
|
}
|
||||||
if caName == "" {
|
if caName == "" {
|
||||||
return errors.New("CA name is required")
|
return errors.New("CA name is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inherit AIA URL from config when not provided on CLI.
|
|
||||||
if aiaBaseURL == "" {
|
if aiaBaseURL == "" {
|
||||||
aiaBaseURL = cfg.data.AIABaseURL
|
aiaBaseURL = readAIABaseURL(caDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
rootCertPath := filepath.Join(caDir, "ca_cert.pem")
|
rootCertPath := filepath.Join(caDir, "ca_cert.pem")
|
||||||
@@ -241,10 +184,10 @@ func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string, cfg *C
|
|||||||
if !fileExists(rootCertPath) || !fileExists(rootKeyPath) {
|
if !fileExists(rootCertPath) || !fileExists(rootKeyPath) {
|
||||||
if issuingCA != "" {
|
if issuingCA != "" {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"cannot create issuing CA '%s' without existing root CA certificate and key. "+
|
"cannot create issuing CA '%s' without existing root CA. "+
|
||||||
"Please create the root CA first", caName)
|
"Please create the root CA first", caName)
|
||||||
}
|
}
|
||||||
fmt.Printf("Generating CA certificate '%s' and key...\n", caName)
|
fmt.Printf("Generating root CA certificate '%s' and key...\n", caName)
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -274,10 +217,12 @@ func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string, cfg *C
|
|||||||
if err := writeCert(der, rootCertPath); err != nil {
|
if err := writeCert(der, rootCertPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := rebuildCABundle(caDir, cfg); err != nil {
|
if aiaBaseURL != "" {
|
||||||
return err
|
if err := writeAIABaseURL(caDir, aiaBaseURL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return cfg.setAIABaseURL(aiaBaseURL)
|
return rebuildCABundle(caDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- issuing CA ----
|
// ---- issuing CA ----
|
||||||
@@ -285,64 +230,57 @@ func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string, cfg *C
|
|||||||
issuingCACert := filepath.Join(issuingCADir, "ca_cert.pem")
|
issuingCACert := filepath.Join(issuingCADir, "ca_cert.pem")
|
||||||
issuingCAKey := filepath.Join(issuingCADir, "ca_key.pem")
|
issuingCAKey := filepath.Join(issuingCADir, "ca_key.pem")
|
||||||
|
|
||||||
if !fileExists(issuingCACert) || !fileExists(issuingCAKey) {
|
if fileExists(issuingCACert) && fileExists(issuingCAKey) {
|
||||||
fmt.Printf("Generating issuing CA certificate '%s' and key...\n", caName)
|
fmt.Printf("Issuing CA '%s' already exists in %s, skipping.\n", issuingCA, issuingCADir)
|
||||||
if err := os.MkdirAll(issuingCADir, 0o755); err != nil {
|
return nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
rootCert, err := loadCert(rootCertPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rootKey, err := loadKey(rootKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
serial, err := randomSerial()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
now := time.Now().UTC()
|
|
||||||
tmpl := &x509.Certificate{
|
|
||||||
SerialNumber: serial,
|
|
||||||
Subject: pkix.Name{CommonName: caName},
|
|
||||||
NotBefore: now,
|
|
||||||
NotAfter: now.AddDate(0, 0, days),
|
|
||||||
IsCA: true,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
MaxPathLen: 0,
|
|
||||||
MaxPathLenZero: true,
|
|
||||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
|
||||||
}
|
|
||||||
if aiaBaseURL != "" {
|
|
||||||
tmpl.IssuingCertificateURL = []string{aiaBaseURL + "/ca_cert.crt"}
|
|
||||||
}
|
|
||||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, rootCert, &key.PublicKey, rootKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := writeKey(key, issuingCAKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := writeCert(der, issuingCACert); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cfg.addSubordinate(issuingCA); err != nil {
|
fmt.Printf("Generating issuing CA certificate '%s' and key...\n", caName)
|
||||||
|
if err := os.MkdirAll(issuingCADir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if aiaBaseURL != "" && aiaBaseURL != cfg.data.AIABaseURL {
|
rootCert, err := loadCert(rootCertPath)
|
||||||
cfg.data.AIABaseURL = aiaBaseURL
|
if err != nil {
|
||||||
if err := cfg.save(); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return rebuildCABundle(caDir, cfg)
|
rootKey, err := loadKey(rootKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
serial, err := randomSerial()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
now := time.Now().UTC()
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
SerialNumber: serial,
|
||||||
|
Subject: pkix.Name{CommonName: caName},
|
||||||
|
NotBefore: now,
|
||||||
|
NotAfter: now.AddDate(0, 0, days),
|
||||||
|
IsCA: true,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
MaxPathLen: 0,
|
||||||
|
MaxPathLenZero: true,
|
||||||
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||||
|
}
|
||||||
|
if aiaBaseURL != "" {
|
||||||
|
tmpl.IssuingCertificateURL = []string{aiaBaseURL + "/ca_cert.crt"}
|
||||||
|
}
|
||||||
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, rootCert, &key.PublicKey, rootKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeKey(key, issuingCAKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeCert(der, issuingCACert); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return rebuildCABundle(caDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- makeCert ---------------------------------------------------------------
|
// ---- makeCert ---------------------------------------------------------------
|
||||||
@@ -352,12 +290,9 @@ var (
|
|||||||
dnsRE = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)*$`)
|
dnsRE = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)*$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeCert(certDir, subjectName string, sans []string, caDir, issuingCA string, days int, cfg *Config) error {
|
func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA string, days int) error {
|
||||||
if issuingCA == "ca" {
|
if issuingCA == "ca" {
|
||||||
return errors.New("--issuing-ca cannot be 'ca' as it is reserved for the root CA")
|
return errors.New("--issuing-ca cannot be 'ca'")
|
||||||
}
|
|
||||||
if certDir == "" || !dirExists(certDir) {
|
|
||||||
return fmt.Errorf("certificate directory %s does not exist", certDir)
|
|
||||||
}
|
}
|
||||||
if !dirExists(caDir) {
|
if !dirExists(caDir) {
|
||||||
return fmt.Errorf("CA directory %s does not exist", caDir)
|
return fmt.Errorf("CA directory %s does not exist", caDir)
|
||||||
@@ -373,6 +308,13 @@ func makeCert(certDir, subjectName string, sans []string, caDir, issuingCA strin
|
|||||||
if issuingCA != "" {
|
if issuingCA != "" {
|
||||||
signingDir = filepath.Join(caDir, issuingCA)
|
signingDir = filepath.Join(caDir, issuingCA)
|
||||||
}
|
}
|
||||||
|
if certDir == "" {
|
||||||
|
certDir = signingDir
|
||||||
|
}
|
||||||
|
if !dirExists(certDir) {
|
||||||
|
return fmt.Errorf("certificate directory %s does not exist", certDir)
|
||||||
|
}
|
||||||
|
|
||||||
caCertPath := filepath.Join(signingDir, "ca_cert.pem")
|
caCertPath := filepath.Join(signingDir, "ca_cert.pem")
|
||||||
caKeyPath := filepath.Join(signingDir, "ca_key.pem")
|
caKeyPath := filepath.Join(signingDir, "ca_key.pem")
|
||||||
if !fileExists(caCertPath) || !fileExists(caKeyPath) {
|
if !fileExists(caCertPath) || !fileExists(caKeyPath) {
|
||||||
@@ -382,7 +324,7 @@ func makeCert(certDir, subjectName string, sans []string, caDir, issuingCA strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
aiaURL := ""
|
aiaURL := ""
|
||||||
if base := cfg.data.AIABaseURL; base != "" {
|
if base := readAIABaseURL(caDir); base != "" {
|
||||||
if issuingCA != "" {
|
if issuingCA != "" {
|
||||||
aiaURL = base + "/" + issuingCA + "/ca_cert.crt"
|
aiaURL = base + "/" + issuingCA + "/ca_cert.crt"
|
||||||
} else {
|
} else {
|
||||||
@@ -419,6 +361,7 @@ func makeCert(certDir, subjectName string, sans []string, caDir, issuingCA strin
|
|||||||
certOut := filepath.Join(certDir, certName+"_cert.pem")
|
certOut := filepath.Join(certDir, certName+"_cert.pem")
|
||||||
keyOut := filepath.Join(certDir, certName+"_key.pem")
|
keyOut := filepath.Join(certDir, certName+"_key.pem")
|
||||||
if fileExists(certOut) && fileExists(keyOut) {
|
if fileExists(certOut) && fileExists(keyOut) {
|
||||||
|
fmt.Printf("Certificate already exists in %s, skipping.\n", certDir)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +412,7 @@ func makeCert(certDir, subjectName string, sans []string, caDir, issuingCA strin
|
|||||||
|
|
||||||
func makePFX(certPath, caDir, issuingCA, password string, appleOpenSSL bool) error {
|
func makePFX(certPath, caDir, issuingCA, password string, appleOpenSSL bool) error {
|
||||||
if issuingCA == "ca" {
|
if issuingCA == "ca" {
|
||||||
return errors.New("--issuing-ca cannot be 'ca' as it is reserved for the root CA")
|
return errors.New("--issuing-ca cannot be 'ca'")
|
||||||
}
|
}
|
||||||
|
|
||||||
certDir := filepath.Dir(certPath)
|
certDir := filepath.Dir(certPath)
|
||||||
@@ -488,7 +431,7 @@ func makePFX(certPath, caDir, issuingCA, password string, appleOpenSSL bool) err
|
|||||||
return errors.New("server certificate or key not found")
|
return errors.New("server certificate or key not found")
|
||||||
}
|
}
|
||||||
if !fileExists(rootCertPath) {
|
if !fileExists(rootCertPath) {
|
||||||
return fmt.Errorf("CA certificate not found in %s", caDir)
|
return fmt.Errorf("root CA certificate not found in %s", caDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
var issuingCACertPath string
|
var issuingCACertPath string
|
||||||
@@ -502,16 +445,14 @@ func makePFX(certPath, caDir, issuingCA, password string, appleOpenSSL bool) err
|
|||||||
if password == "" {
|
if password == "" {
|
||||||
password = "changeit"
|
password = "changeit"
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileExists(pfxPath) {
|
if fileExists(pfxPath) {
|
||||||
fmt.Println("PKCS#12 (PFX) file already exists, aborting generation.")
|
return errors.New("PKCS#12 (PFX) file already exists, aborting generation")
|
||||||
return errors.New("PFX file already exists")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Generating PKCS#12 (PFX) file...")
|
fmt.Print("Generating PKCS#12 (PFX) file...")
|
||||||
|
|
||||||
if appleOpenSSL {
|
if appleOpenSSL {
|
||||||
return makePFXViaAppleOpenSSL(certPath, keyPath, rootCertPath, issuingCACertPath, pfxPath, password)
|
return makePFXViaOpenSSL("/usr/bin/openssl", certPath, keyPath, rootCertPath, issuingCACertPath, pfxPath, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := loadCert(certPath)
|
cert, err := loadCert(certPath)
|
||||||
@@ -546,7 +487,7 @@ func makePFX(certPath, caDir, issuingCA, password string, appleOpenSSL bool) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func makePFXViaAppleOpenSSL(certPath, keyPath, rootCertPath, issuingCACertPath, pfxPath, password string) error {
|
func makePFXViaOpenSSL(opensslBin, certPath, keyPath, rootCertPath, issuingCACertPath, pfxPath, password string) error {
|
||||||
chainFile, err := os.CreateTemp("", "chain-*.pem")
|
chainFile, err := os.CreateTemp("", "chain-*.pem")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -575,7 +516,7 @@ func makePFXViaAppleOpenSSL(certPath, keyPath, rootCertPath, issuingCACertPath,
|
|||||||
}
|
}
|
||||||
chainFile.Close()
|
chainFile.Close()
|
||||||
|
|
||||||
cmd := exec.Command("/usr/bin/openssl", "pkcs12",
|
cmd := exec.Command(opensslBin, "pkcs12",
|
||||||
"-export", "-out", pfxPath,
|
"-export", "-out", pfxPath,
|
||||||
"-inkey", keyPath,
|
"-inkey", keyPath,
|
||||||
"-in", certPath,
|
"-in", certPath,
|
||||||
@@ -585,7 +526,7 @@ func makePFXViaAppleOpenSSL(certPath, keyPath, rootCertPath, issuingCACertPath,
|
|||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("apple openssl pkcs12 failed: %w", err)
|
return fmt.Errorf("openssl pkcs12 failed: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Println("done.")
|
fmt.Println("done.")
|
||||||
return nil
|
return nil
|
||||||
@@ -616,102 +557,56 @@ func newRootCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newMakeCACmd() *cobra.Command {
|
func newMakeCACmd() *cobra.Command {
|
||||||
var (
|
var days int
|
||||||
days int
|
var issuingCA, aiaBaseURL, caDir string
|
||||||
issuingCA string
|
|
||||||
aiaBaseURL string
|
|
||||||
caDir string
|
|
||||||
)
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "make-ca CA_NAME",
|
Use: "make-ca CA_NAME",
|
||||||
Short: "Create a root or issuing CA.",
|
Short: "Create a root or issuing CA.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(c *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
dir := resolveCADir(caDir)
|
return makeCA(resolveCADir(caDir), args[0], days, issuingCA, aiaBaseURL)
|
||||||
cfg := loadConfig(dir)
|
|
||||||
effectiveDays := days
|
|
||||||
if !c.Flags().Changed("days") {
|
|
||||||
if cfg.data.Days != nil && cfg.data.Days.CA > 0 {
|
|
||||||
effectiveDays = cfg.data.Days.CA
|
|
||||||
} else {
|
|
||||||
effectiveDays = 3650
|
|
||||||
}
|
|
||||||
}
|
|
||||||
effectiveIssuingCA := issuingCA
|
|
||||||
if !c.Flags().Changed("issuing-ca") && cfg.data.IssuingCA != "" {
|
|
||||||
effectiveIssuingCA = cfg.data.IssuingCA
|
|
||||||
}
|
|
||||||
return makeCA(dir, args[0], effectiveDays, effectiveIssuingCA, aiaBaseURL, cfg)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().IntVar(&days, "days", 3650, "validity period in days")
|
cmd.Flags().IntVar(&days, "days", 3650, "validity period in days")
|
||||||
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name (creates an issuing CA signed by the root)")
|
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "create an issuing CA with this directory name")
|
||||||
cmd.Flags().StringVar(&aiaBaseURL, "aia-base-url", "", "base URL for the AIA caIssuers extension")
|
cmd.Flags().StringVar(&aiaBaseURL, "aia-base-url", "", "base URL for AIA caIssuers extension")
|
||||||
cmd.Flags().StringVar(&caDir, "ca-dir", "", "directory for CA files")
|
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA root directory")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMakeCertCmd() *cobra.Command {
|
func newMakeCertCmd() *cobra.Command {
|
||||||
var (
|
var certDir, caDir, issuingCA string
|
||||||
certDir string
|
var days int
|
||||||
caDir string
|
|
||||||
issuingCA string
|
|
||||||
days int
|
|
||||||
)
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "make-cert SUBJECT [SAN...]",
|
Use: "make-cert SUBJECT [SAN...]",
|
||||||
Short: "Create a server/client certificate signed by the CA.",
|
Short: "Create a server/client certificate signed by the CA.",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
RunE: func(c *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
dir := resolveCADir(caDir)
|
return makeCert(args[0], args[1:], resolveCADir(caDir), certDir, issuingCA, days)
|
||||||
cfg := loadConfig(dir)
|
|
||||||
effectiveDays := days
|
|
||||||
if !c.Flags().Changed("days") {
|
|
||||||
if cfg.data.Days != nil && cfg.data.Days.Cert > 0 {
|
|
||||||
effectiveDays = cfg.data.Days.Cert
|
|
||||||
} else {
|
|
||||||
effectiveDays = 365
|
|
||||||
}
|
|
||||||
}
|
|
||||||
effectiveIssuingCA := issuingCA
|
|
||||||
if !c.Flags().Changed("issuing-ca") && cfg.data.IssuingCA != "" {
|
|
||||||
effectiveIssuingCA = cfg.data.IssuingCA
|
|
||||||
}
|
|
||||||
return makeCert(certDir, args[0], args[1:], dir, effectiveIssuingCA, effectiveDays, cfg)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVar(&certDir, "cert-dir", "", "directory to store the certificate files")
|
cmd.Flags().StringVar(&certDir, "cert-dir", "", "output directory (default: signing CA directory)")
|
||||||
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA directory")
|
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA root directory")
|
||||||
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name")
|
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name")
|
||||||
cmd.Flags().IntVar(&days, "days", 365, "validity period in days")
|
cmd.Flags().IntVar(&days, "days", 365, "validity period in days")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMakePFXCmd() *cobra.Command {
|
func newMakePFXCmd() *cobra.Command {
|
||||||
var (
|
var caDir, issuingCA, password string
|
||||||
caDir string
|
var appleOpenSSL bool
|
||||||
issuingCA string
|
|
||||||
password string
|
|
||||||
appleOpenSSL bool
|
|
||||||
)
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "make-pfx CERT_PATH",
|
Use: "make-pfx CERT_PATH",
|
||||||
Short: "Create a PKCS#12 (PFX) bundle for a leaf certificate.",
|
Short: "Create a PKCS#12 (PFX) bundle for a leaf certificate.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(c *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
dir := resolveCADir(caDir)
|
return makePFX(args[0], resolveCADir(caDir), issuingCA, password, appleOpenSSL)
|
||||||
cfg := loadConfig(dir)
|
|
||||||
effectiveIssuingCA := issuingCA
|
|
||||||
if !c.Flags().Changed("issuing-ca") && cfg.data.IssuingCA != "" {
|
|
||||||
effectiveIssuingCA = cfg.data.IssuingCA
|
|
||||||
}
|
|
||||||
return makePFX(args[0], dir, effectiveIssuingCA, password, appleOpenSSL)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA directory")
|
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA root directory")
|
||||||
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name")
|
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name")
|
||||||
cmd.Flags().StringVar(&password, "password", "", "PFX password (default: changeit)")
|
cmd.Flags().StringVar(&password, "password", "", "PFX password (default: changeit)")
|
||||||
cmd.Flags().BoolVar(&appleOpenSSL, "apple-openssl", false, "use Apple's bundled /usr/bin/openssl for PKCS12 generation")
|
cmd.Flags().BoolVar(&appleOpenSSL, "apple-openssl", false, "use /usr/bin/openssl for PKCS12 (Apple-compatible format)")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func verifyCert(t *testing.T, bundle, cert string) {
|
||||||
|
t.Helper()
|
||||||
|
out, err := exec.Command("openssl", "verify", "-CAfile", bundle, cert).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("certificate verification failed for %s:\n%s", cert, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStandaloneCA(t *testing.T) {
|
||||||
|
caDir := t.TempDir()
|
||||||
|
|
||||||
|
if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil {
|
||||||
|
t.Fatalf("makeCA: %v", err)
|
||||||
|
}
|
||||||
|
if !fileExists(filepath.Join(caDir, "ca_cert.pem")) {
|
||||||
|
t.Fatal("ca_cert.pem not created")
|
||||||
|
}
|
||||||
|
if !fileExists(filepath.Join(caDir, "ca_bundle.pem")) {
|
||||||
|
t.Fatal("ca_bundle.pem not created")
|
||||||
|
}
|
||||||
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), filepath.Join(caDir, "ca_cert.pem"))
|
||||||
|
|
||||||
|
if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "", 365); err != nil {
|
||||||
|
t.Fatalf("makeCert: %v", err)
|
||||||
|
}
|
||||||
|
certPath := filepath.Join(caDir, "test_cert.pem")
|
||||||
|
if !fileExists(certPath) {
|
||||||
|
t.Fatal("test_cert.pem not created in caDir")
|
||||||
|
}
|
||||||
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), certPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTwoLevelCA(t *testing.T) {
|
||||||
|
caDir := t.TempDir()
|
||||||
|
|
||||||
|
if err := makeCA(caDir, "Test Root CA", 3650, "", ""); err != nil {
|
||||||
|
t.Fatalf("makeCA root: %v", err)
|
||||||
|
}
|
||||||
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), filepath.Join(caDir, "ca_cert.pem"))
|
||||||
|
|
||||||
|
if err := makeCA(caDir, "Issuing CA", 3650, "issuing_ca", ""); err != nil {
|
||||||
|
t.Fatalf("makeCA issuing: %v", err)
|
||||||
|
}
|
||||||
|
issuingCert := filepath.Join(caDir, "issuing_ca", "ca_cert.pem")
|
||||||
|
if !fileExists(issuingCert) {
|
||||||
|
t.Fatal("issuing_ca/ca_cert.pem not created")
|
||||||
|
}
|
||||||
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), issuingCert)
|
||||||
|
|
||||||
|
if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "issuing_ca", 365); err != nil {
|
||||||
|
t.Fatalf("makeCert: %v", err)
|
||||||
|
}
|
||||||
|
certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem")
|
||||||
|
if !fileExists(certPath) {
|
||||||
|
t.Fatal("issuing_ca/test_cert.pem not created")
|
||||||
|
}
|
||||||
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), certPath)
|
||||||
|
|
||||||
|
if err := makePFX(certPath, caDir, "issuing_ca", "s3cr3t", false); err != nil {
|
||||||
|
t.Fatalf("makePFX: %v", err)
|
||||||
|
}
|
||||||
|
pfxPath := filepath.Join(caDir, "issuing_ca", "test.pfx")
|
||||||
|
if !fileExists(pfxPath) {
|
||||||
|
t.Fatal("issuing_ca/test.pfx not created")
|
||||||
|
}
|
||||||
|
out, err := exec.Command("openssl", "pkcs12", "-in", pfxPath, "-noout", "-info",
|
||||||
|
"-password", "pass:s3cr3t").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PFX verification failed:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertDirOverride(t *testing.T) {
|
||||||
|
caDir := t.TempDir()
|
||||||
|
certDir := t.TempDir()
|
||||||
|
|
||||||
|
if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil {
|
||||||
|
t.Fatalf("makeCA: %v", err)
|
||||||
|
}
|
||||||
|
if err := makeCert("test", []string{"test.example.com"}, caDir, certDir, "", 365); err != nil {
|
||||||
|
t.Fatalf("makeCert: %v", err)
|
||||||
|
}
|
||||||
|
certPath := filepath.Join(certDir, "test_cert.pem")
|
||||||
|
if !fileExists(certPath) {
|
||||||
|
t.Fatal("test_cert.pem not created in certDir override")
|
||||||
|
}
|
||||||
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), certPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppleOpenSSL(t *testing.T) {
|
||||||
|
if _, err := os.Stat("/usr/bin/openssl"); err != nil {
|
||||||
|
t.Skip("/usr/bin/openssl not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
caDir := t.TempDir()
|
||||||
|
|
||||||
|
if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil {
|
||||||
|
t.Fatalf("makeCA: %v", err)
|
||||||
|
}
|
||||||
|
if err := makeCA(caDir, "Issuing CA", 3650, "issuing_ca", ""); err != nil {
|
||||||
|
t.Fatalf("makeCA issuing: %v", err)
|
||||||
|
}
|
||||||
|
if err := makeCert("test", []string{"test.example.com"}, caDir, "", "issuing_ca", 365); err != nil {
|
||||||
|
t.Fatalf("makeCert: %v", err)
|
||||||
|
}
|
||||||
|
certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem")
|
||||||
|
|
||||||
|
if err := makePFX(certPath, caDir, "issuing_ca", "s3cr3t", true); err != nil {
|
||||||
|
t.Fatalf("makePFX --apple-openssl: %v", err)
|
||||||
|
}
|
||||||
|
pfxPath := filepath.Join(caDir, "issuing_ca", "test.pfx")
|
||||||
|
out, err := exec.Command("/usr/bin/openssl", "pkcs12", "-in", pfxPath, "-noout", "-info",
|
||||||
|
"-password", "pass:s3cr3t").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Apple openssl PFX verification failed:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-34
@@ -35,10 +35,8 @@ def verify_cert(cert_path, bundle_path):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def dirs(tmp_path):
|
def dirs(tmp_path):
|
||||||
ca = tmp_path / "ca"
|
ca = tmp_path / "ca"
|
||||||
certs = tmp_path / "certs"
|
|
||||||
ca.mkdir()
|
ca.mkdir()
|
||||||
certs.mkdir()
|
return ca
|
||||||
return ca, certs
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -46,7 +44,7 @@ def dirs(tmp_path):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_standalone_ca(dirs):
|
def test_standalone_ca(dirs):
|
||||||
ca, certs = dirs
|
ca = dirs
|
||||||
py("make-ca", "--ca-dir", str(ca), "Test CA")
|
py("make-ca", "--ca-dir", str(ca), "Test CA")
|
||||||
|
|
||||||
assert (ca / "ca_cert.pem").exists()
|
assert (ca / "ca_cert.pem").exists()
|
||||||
@@ -54,11 +52,10 @@ def test_standalone_ca(dirs):
|
|||||||
assert (ca / "simple-ca.json").exists()
|
assert (ca / "simple-ca.json").exists()
|
||||||
verify_cert(ca / "ca_cert.pem", ca / "ca_bundle.pem")
|
verify_cert(ca / "ca_cert.pem", ca / "ca_bundle.pem")
|
||||||
|
|
||||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
py("make-cert", "--ca-dir", str(ca), "test", "test.example.com", "127.0.0.1")
|
||||||
"test", "test.example.com", "127.0.0.1")
|
|
||||||
|
|
||||||
assert (certs / "test_cert.pem").exists()
|
assert (ca / "test_cert.pem").exists()
|
||||||
verify_cert(certs / "test_cert.pem", ca / "ca_bundle.pem")
|
verify_cert(ca / "test_cert.pem", ca / "ca_bundle.pem")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -66,23 +63,23 @@ def test_standalone_ca(dirs):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_two_level_ca(dirs):
|
def test_two_level_ca(dirs):
|
||||||
ca, certs = dirs
|
ca = dirs
|
||||||
py("make-ca", "--ca-dir", str(ca), "Test Root CA")
|
py("make-ca", "--ca-dir", str(ca), "Test Root CA")
|
||||||
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
|
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
|
||||||
|
|
||||||
assert (ca / "issuing_ca" / "ca_cert.pem").exists()
|
assert (ca / "issuing_ca" / "ca_cert.pem").exists()
|
||||||
verify_cert(ca / "issuing_ca" / "ca_cert.pem", ca / "ca_bundle.pem")
|
verify_cert(ca / "issuing_ca" / "ca_cert.pem", ca / "ca_bundle.pem")
|
||||||
|
|
||||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||||
"--issuing-ca", "issuing_ca", "test", "test.example.com", "127.0.0.1")
|
"test", "test.example.com", "127.0.0.1")
|
||||||
|
|
||||||
verify_cert(certs / "test_cert.pem", ca / "ca_bundle.pem")
|
verify_cert(ca / "issuing_ca" / "test_cert.pem", ca / "ca_bundle.pem")
|
||||||
|
|
||||||
py("make-pfx", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
py("make-pfx", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||||
"--password", "s3cr3t", str(certs / "test_cert.pem"))
|
"--password", "s3cr3t", str(ca / "issuing_ca" / "test_cert.pem"))
|
||||||
|
|
||||||
assert (certs / "test.pfx").exists()
|
assert (ca / "issuing_ca" / "test.pfx").exists()
|
||||||
result = openssl("pkcs12", "-in", str(certs / "test.pfx"), "-noout", "-info",
|
result = openssl("pkcs12", "-in", str(ca / "issuing_ca" / "test.pfx"), "-noout", "-info",
|
||||||
"-password", "pass:s3cr3t")
|
"-password", "pass:s3cr3t")
|
||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
|
|
||||||
@@ -92,31 +89,31 @@ def test_two_level_ca(dirs):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_pfx_modern(dirs):
|
def test_pfx_modern(dirs):
|
||||||
ca, certs = dirs
|
ca = dirs
|
||||||
py("make-ca", "--ca-dir", str(ca), "PFX Test CA")
|
py("make-ca", "--ca-dir", str(ca), "PFX Test CA")
|
||||||
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
|
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
|
||||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||||
"--issuing-ca", "issuing_ca", "test", "test.example.com", "127.0.0.1")
|
"test", "test.example.com", "127.0.0.1")
|
||||||
py("make-pfx", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
py("make-pfx", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||||
"--password", "s3cr3t", str(certs / "test_cert.pem"))
|
"--password", "s3cr3t", str(ca / "issuing_ca" / "test_cert.pem"))
|
||||||
|
|
||||||
info = openssl("pkcs12", "-in", str(certs / "test.pfx"), "-noout", "-info",
|
info = openssl("pkcs12", "-in", str(ca / "issuing_ca" / "test.pfx"), "-noout", "-info",
|
||||||
"-password", "pass:s3cr3t")
|
"-password", "pass:s3cr3t")
|
||||||
assert "PBES2" in (info.stdout + info.stderr), "Expected modern PBES2 encryption"
|
assert "PBES2" in (info.stdout + info.stderr), "Expected modern PBES2 encryption"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS only")
|
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS only")
|
||||||
def test_pfx_apple_openssl(dirs):
|
def test_pfx_apple_openssl(dirs):
|
||||||
ca, certs = dirs
|
ca = dirs
|
||||||
py("make-ca", "--ca-dir", str(ca), "PFX Test CA")
|
py("make-ca", "--ca-dir", str(ca), "PFX Test CA")
|
||||||
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
|
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
|
||||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||||
"--issuing-ca", "issuing_ca", "test", "test.example.com", "127.0.0.1")
|
"test", "test.example.com", "127.0.0.1")
|
||||||
py("make-pfx", "--apple-openssl", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
py("make-pfx", "--apple-openssl", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||||
"--password", "s3cr3t", str(certs / "test_cert.pem"))
|
"--password", "s3cr3t", str(ca / "issuing_ca" / "test_cert.pem"))
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/bin/openssl", "pkcs12", "-in", str(certs / "test.pfx"),
|
["/usr/bin/openssl", "pkcs12", "-in", str(ca / "issuing_ca" / "test.pfx"),
|
||||||
"-noout", "-info", "-password", "pass:s3cr3t"],
|
"-noout", "-info", "-password", "pass:s3cr3t"],
|
||||||
capture_output=True, text=True,
|
capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
@@ -130,23 +127,24 @@ def test_pfx_apple_openssl(dirs):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_crl(dirs):
|
def test_crl(dirs):
|
||||||
ca, certs = dirs
|
ca = dirs
|
||||||
py("make-ca", "--ca-dir", str(ca), "CRL Test CA")
|
py("make-ca", "--ca-dir", str(ca), "CRL Test CA")
|
||||||
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
|
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
|
||||||
|
|
||||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||||
"--issuing-ca", "issuing_ca", "alice", "alice.example.com")
|
"alice", "alice.example.com")
|
||||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||||
"--issuing-ca", "issuing_ca", "bob", "bob.example.com")
|
"bob", "bob.example.com")
|
||||||
|
|
||||||
alice_serial = cert_serial(certs / "alice_cert.pem")
|
issuing_dir = ca / "issuing_ca"
|
||||||
bob_serial = cert_serial(certs / "bob_cert.pem")
|
alice_serial = cert_serial(issuing_dir / "alice_cert.pem")
|
||||||
|
bob_serial = cert_serial(issuing_dir / "bob_cert.pem")
|
||||||
|
|
||||||
py("revoke-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
py("revoke-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||||
str(certs / "alice_cert.pem"))
|
str(issuing_dir / "alice_cert.pem"))
|
||||||
|
|
||||||
py("make-crl", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca")
|
py("make-crl", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca")
|
||||||
issuing_crl = ca / "issuing_ca" / "crl.pem"
|
issuing_crl = issuing_dir / "crl.pem"
|
||||||
assert issuing_crl.exists()
|
assert issuing_crl.exists()
|
||||||
|
|
||||||
crl_text = openssl("crl", "-in", str(issuing_crl), "-noout", "-text").stdout.upper()
|
crl_text = openssl("crl", "-in", str(issuing_crl), "-noout", "-text").stdout.upper()
|
||||||
|
|||||||
Reference in New Issue
Block a user