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'
|
||||
- 'run-tests.sh'
|
||||
- 'test_simple_ca.py'
|
||||
- 'src/simple-ca/**'
|
||||
- '.gitea/workflows/test.yaml'
|
||||
|
||||
jobs:
|
||||
@@ -33,3 +34,18 @@ jobs:
|
||||
|
||||
- name: Run shell tests
|
||||
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
|
||||
|
||||
CA_DIR="$TEST_DIR/ca"
|
||||
CERT_DIR="$TEST_DIR/certs"
|
||||
|
||||
reset_dirs() {
|
||||
rm -rf "$CA_DIR" "$CERT_DIR"
|
||||
mkdir -p "$CA_DIR" "$CERT_DIR"
|
||||
rm -rf "$CA_DIR"
|
||||
mkdir -p "$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 "--- [shell] Standalone CA ---"
|
||||
reset_dirs
|
||||
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_bundle.pem" ]] || { echo "ERROR: ca_bundle.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; }
|
||||
verify_cert "$CA_DIR/ca_cert.pem"
|
||||
|
||||
make_cert --cert-dir "$CERT_DIR" "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; }
|
||||
verify_cert "$CERT_DIR/test_cert.pem"
|
||||
make_cert "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
||||
[[ -f "$CA_DIR/test_cert.pem" ]] || { echo "ERROR: test_cert.pem not created in CA_DIR" >&2; exit 1; }
|
||||
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
|
||||
@@ -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; }
|
||||
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
|
||||
verify_cert "$CERT_DIR/test_cert.pem"
|
||||
make_cert --issuing-ca "issuing_ca" "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
||||
[[ -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
|
||||
[[ -f "$CERT_DIR/test.pfx" ]] || { echo "ERROR: test.pfx not created" >&2; exit 1; }
|
||||
openssl pkcs12 -in "$CERT_DIR/test.pfx" -noout -info -password pass:"s3cr3t" 2>/dev/null \
|
||||
make_pfx --issuing-ca "issuing_ca" --password "s3cr3t" "$CA_DIR/issuing_ca/test_cert.pem" 2>/dev/null
|
||||
[[ -f "$CA_DIR/issuing_ca/test.pfx" ]] || { echo "ERROR: issuing_ca/test.pfx not created" >&2; exit 1; }
|
||||
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 "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
|
||||
|
||||
|
||||
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):
|
||||
if issuing_ca == "ca":
|
||||
_err("--issuing-ca cannot be 'ca' as it is reserved for the root CA.")
|
||||
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):
|
||||
_err(f"CA directory {ca_dir} does not exist.")
|
||||
return False
|
||||
@@ -278,6 +272,11 @@ def make_cert(cert_dir, cert_subject_name, sans=None, ca_dir=None,
|
||||
return False
|
||||
|
||||
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_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):
|
||||
@@ -581,9 +580,10 @@ def main(argv=None):
|
||||
elif args.command == "make-cert":
|
||||
days = args.days or days_cfg.get("cert", 365)
|
||||
ok = make_cert(
|
||||
args.cert_dir, args.subject_name,
|
||||
args.subject_name,
|
||||
sans=args.sans,
|
||||
ca_dir=ca_dir,
|
||||
cert_dir=getattr(args, "cert_dir", None),
|
||||
issuing_ca=issuing_ca,
|
||||
days=days,
|
||||
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
|
||||
# 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_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_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.
|
||||
|
||||
SIMPLE_CA_DIR="${SIMPLE_CA_DIR:-}"
|
||||
@@ -65,17 +68,32 @@ function make_ca() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--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 ;;
|
||||
--issuing-ca)
|
||||
[[ -z "$2" ]] && { echo "ERROR: --issuing-ca requires a value." >&2; return 1; }
|
||||
[[ "$2" == "ca" ]] && { echo "ERROR: --issuing-ca cannot be 'ca'." >&2; return 1; }
|
||||
if [[ -z "$2" ]]; then
|
||||
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 ;;
|
||||
--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 ;;
|
||||
--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 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
@@ -84,10 +102,14 @@ function make_ca() {
|
||||
local CA_NAME="$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" ]] \
|
||||
&& AIA_BASE_URL="$(cat "$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")"
|
||||
fi
|
||||
|
||||
local ROOT_CA_CERT="$SIMPLE_CA_DIR/ca_cert.pem"
|
||||
local ROOT_CA_KEY="$SIMPLE_CA_DIR/ca_key.pem"
|
||||
@@ -114,7 +136,9 @@ function make_ca() {
|
||||
return 1
|
||||
fi
|
||||
_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
|
||||
fi
|
||||
|
||||
@@ -167,38 +191,62 @@ function make_cert() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--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 ;;
|
||||
--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 ;;
|
||||
--issuing-ca)
|
||||
[[ -z "$2" ]] && { echo "ERROR: --issuing-ca requires a value." >&2; return 1; }
|
||||
[[ "$2" == "ca" ]] && { echo "ERROR: --issuing-ca cannot be 'ca'." >&2; return 1; }
|
||||
if [[ -z "$2" ]]; then
|
||||
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 ;;
|
||||
--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 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
local CERT_SUBJECT_NAME="$1"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
if [[ $# -gt 0 ]]; then
|
||||
shift
|
||||
fi
|
||||
|
||||
_require_ca_dir || return 1
|
||||
|
||||
[[ -z "$CERT_DIR" ]] && { echo "ERROR: --cert-dir is required." >&2; return 1; }
|
||||
[[ ! -d "$CERT_DIR" ]] && { echo "ERROR: Certificate directory '$CERT_DIR' does not exist." >&2; return 1; }
|
||||
[[ -z "$CERT_SUBJECT_NAME" ]] && { echo "ERROR: Subject name is required." >&2; return 1; }
|
||||
_is_dns "$CERT_SUBJECT_NAME" || { echo "ERROR: Invalid subject name '$CERT_SUBJECT_NAME'. Must be a valid DNS name." >&2; return 1; }
|
||||
if [[ -z "$CERT_SUBJECT_NAME" ]]; then
|
||||
echo "ERROR: Subject name is required." >&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_CERT="$SIGNING_DIR/ca_cert.pem"
|
||||
local SIGNING_KEY="$SIGNING_DIR/ca_key.pem"
|
||||
CERT_DIR="${CERT_DIR:-$SIGNING_DIR}"
|
||||
|
||||
[[ ! -f "$SIGNING_CERT" || ! -f "$SIGNING_KEY" ]] \
|
||||
&& { echo "ERROR: Signing CA certificate and key not found in $SIGNING_DIR." >&2; return 1; }
|
||||
if [[ ! -f "$SIGNING_CERT" || ! -f "$SIGNING_KEY" ]]; then
|
||||
echo "ERROR: Signing CA certificate and key not found in $SIGNING_DIR." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local AIA_URL=""
|
||||
if [[ -f "$SIMPLE_CA_DIR/aia_base_url.txt" ]]; then
|
||||
@@ -211,9 +259,13 @@ function make_cert() {
|
||||
local SANS=("DNS:${CERT_SUBJECT_NAME}")
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
if _is_ip "$1"; then SANS+=("IP:$1")
|
||||
elif _is_dns "$1"; then SANS+=("DNS:$1")
|
||||
else { echo "ERROR: Invalid SAN entry '$1'." >&2; return 1; }
|
||||
if _is_ip "$1"; then
|
||||
SANS+=("IP:$1")
|
||||
elif _is_dns "$1"; then
|
||||
SANS+=("DNS:$1")
|
||||
else
|
||||
echo "ERROR: Invalid SAN entry '$1'." >&2
|
||||
return 1
|
||||
fi
|
||||
shift
|
||||
done
|
||||
@@ -255,19 +307,34 @@ function make_cert() {
|
||||
function make_pfx() {
|
||||
local ISSUING_CA=""
|
||||
local PFX_PASSWORD=""
|
||||
local APPLE_OPENSSL=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--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 ;;
|
||||
--issuing-ca)
|
||||
[[ -z "$2" ]] && { echo "ERROR: --issuing-ca requires a value." >&2; return 1; }
|
||||
[[ "$2" == "ca" ]] && { echo "ERROR: --issuing-ca cannot be 'ca'." >&2; return 1; }
|
||||
if [[ -z "$2" ]]; then
|
||||
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 ;;
|
||||
--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 ;;
|
||||
--apple-openssl)
|
||||
APPLE_OPENSSL=1; shift ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
@@ -275,24 +342,44 @@ function make_pfx() {
|
||||
local CERT_PATH="$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
|
||||
CERT_DIR="$(dirname "$CERT_PATH")"
|
||||
CERT_NAME="$(basename "$CERT_PATH" _cert.pem)"
|
||||
KEY_PATH="$CERT_DIR/${CERT_NAME}_key.pem"
|
||||
|
||||
[[ ! -d "$CERT_DIR" ]] && { echo "ERROR: Certificate directory '$CERT_DIR' does not exist." >&2; return 1; }
|
||||
[[ ! -f "$CERT_PATH" || ! -f "$KEY_PATH" ]] && { echo "ERROR: Server certificate or key not found." >&2; return 1; }
|
||||
[[ ! -f "$SIMPLE_CA_DIR/ca_cert.pem" ]] && { echo "ERROR: Root CA certificate not found in $SIMPLE_CA_DIR." >&2; return 1; }
|
||||
|
||||
[[ -n "$ISSUING_CA" && ! -f "$SIMPLE_CA_DIR/$ISSUING_CA/ca_cert.pem" ]] \
|
||||
&& { echo "ERROR: Issuing CA certificate not found in $SIMPLE_CA_DIR/$ISSUING_CA." >&2; return 1; }
|
||||
|
||||
[[ -f "$CERT_DIR/${CERT_NAME}.pfx" ]] && { echo "PKCS#12 (PFX) file already exists, aborting generation." >&2; return 1; }
|
||||
if [[ ! -d "$CERT_DIR" ]]; then
|
||||
echo "ERROR: Certificate directory '$CERT_DIR' does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$CERT_PATH" || ! -f "$KEY_PATH" ]]; then
|
||||
echo "ERROR: Server certificate or key not found." >&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}"
|
||||
|
||||
local OPENSSL_BIN="openssl"
|
||||
if [[ "$APPLE_OPENSSL" -eq 1 ]]; then
|
||||
OPENSSL_BIN="/usr/bin/openssl"
|
||||
fi
|
||||
|
||||
echo -n "Generating PKCS#12 (PFX) file..."
|
||||
|
||||
local CHAIN_FILE
|
||||
@@ -300,9 +387,11 @@ function make_pfx() {
|
||||
trap "rm -f '$CHAIN_FILE'" EXIT QUIT KILL INT HUP
|
||||
|
||||
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 \
|
||||
-out "$CERT_DIR/${CERT_NAME}.pfx" \
|
||||
-inkey "$KEY_PATH" \
|
||||
|
||||
+114
-219
@@ -27,7 +27,6 @@ import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -37,7 +36,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -113,85 +111,27 @@ func loadKey(path string) (*rsa.PrivateKey, error) {
|
||||
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
}
|
||||
|
||||
// ---- Config -----------------------------------------------------------------
|
||||
// ---- AIA base URL -----------------------------------------------------------
|
||||
|
||||
type daysConfig struct {
|
||||
CA int `json:"ca,omitempty"`
|
||||
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)
|
||||
func readAIABaseURL(caDir string) string {
|
||||
b, err := os.ReadFile(filepath.Join(caDir, "aia_base_url.txt"))
|
||||
if err != nil {
|
||||
cfg.isNew = true
|
||||
return cfg
|
||||
return ""
|
||||
}
|
||||
if err := json.Unmarshal(b, &cfg.data); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: could not read %s: %v\n", cfg.path, err)
|
||||
cfg.isNew = true
|
||||
}
|
||||
return cfg
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
|
||||
func (c *Config) save() error {
|
||||
b, err := json.MarshalIndent(c.data, "", " ")
|
||||
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()
|
||||
func writeAIABaseURL(caDir, url string) error {
|
||||
return os.WriteFile(filepath.Join(caDir, "aia_base_url.txt"), []byte(url+"\n"), 0o644)
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
|
||||
rootPath := filepath.Join(caDir, "ca_cert.pem")
|
||||
if fileExists(rootPath) {
|
||||
data, err := os.ReadFile(rootPath)
|
||||
@@ -200,10 +140,13 @@ func rebuildCABundle(caDir string, cfg *Config) error {
|
||||
}
|
||||
bundle = append(bundle, data...)
|
||||
}
|
||||
subs := append([]string(nil), cfg.data.Subordinates...)
|
||||
sort.Strings(subs)
|
||||
for _, name := range subs {
|
||||
subCert := filepath.Join(caDir, name, "ca_cert.pem")
|
||||
|
||||
entries, _ := os.ReadDir(caDir)
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
subCert := filepath.Join(caDir, e.Name(), "ca_cert.pem")
|
||||
if !fileExists(subCert) {
|
||||
continue
|
||||
}
|
||||
@@ -213,25 +156,25 @@ func rebuildCABundle(caDir string, cfg *Config) error {
|
||||
}
|
||||
bundle = append(bundle, data...)
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath.Join(caDir, "ca_bundle.pem"), bundle, 0o644)
|
||||
}
|
||||
|
||||
// ---- 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" {
|
||||
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) {
|
||||
return fmt.Errorf("certificate directory %s does not exist", caDir)
|
||||
if !dirExists(caDir) {
|
||||
return fmt.Errorf("CA directory %s does not exist", caDir)
|
||||
}
|
||||
if caName == "" {
|
||||
return errors.New("CA name is required")
|
||||
}
|
||||
|
||||
// Inherit AIA URL from config when not provided on CLI.
|
||||
if aiaBaseURL == "" {
|
||||
aiaBaseURL = cfg.data.AIABaseURL
|
||||
aiaBaseURL = readAIABaseURL(caDir)
|
||||
}
|
||||
|
||||
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 issuingCA != "" {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if err := rebuildCABundle(caDir, cfg); err != nil {
|
||||
return err
|
||||
if aiaBaseURL != "" {
|
||||
if err := writeAIABaseURL(caDir, aiaBaseURL); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return cfg.setAIABaseURL(aiaBaseURL)
|
||||
return rebuildCABundle(caDir)
|
||||
}
|
||||
|
||||
// ---- 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")
|
||||
issuingCAKey := filepath.Join(issuingCADir, "ca_key.pem")
|
||||
|
||||
if !fileExists(issuingCACert) || !fileExists(issuingCAKey) {
|
||||
fmt.Printf("Generating issuing CA certificate '%s' and key...\n", caName)
|
||||
if err := os.MkdirAll(issuingCADir, 0o755); err != 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 fileExists(issuingCACert) && fileExists(issuingCAKey) {
|
||||
fmt.Printf("Issuing CA '%s' already exists in %s, skipping.\n", issuingCA, issuingCADir)
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if aiaBaseURL != "" && aiaBaseURL != cfg.data.AIABaseURL {
|
||||
cfg.data.AIABaseURL = aiaBaseURL
|
||||
if err := cfg.save(); err != nil {
|
||||
return err
|
||||
}
|
||||
rootCert, err := loadCert(rootCertPath)
|
||||
if err != nil {
|
||||
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 ---------------------------------------------------------------
|
||||
@@ -352,12 +290,9 @@ var (
|
||||
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" {
|
||||
return errors.New("--issuing-ca cannot be 'ca' as it is reserved for the root CA")
|
||||
}
|
||||
if certDir == "" || !dirExists(certDir) {
|
||||
return fmt.Errorf("certificate directory %s does not exist", certDir)
|
||||
return errors.New("--issuing-ca cannot be 'ca'")
|
||||
}
|
||||
if !dirExists(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 != "" {
|
||||
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")
|
||||
caKeyPath := filepath.Join(signingDir, "ca_key.pem")
|
||||
if !fileExists(caCertPath) || !fileExists(caKeyPath) {
|
||||
@@ -382,7 +324,7 @@ func makeCert(certDir, subjectName string, sans []string, caDir, issuingCA strin
|
||||
}
|
||||
|
||||
aiaURL := ""
|
||||
if base := cfg.data.AIABaseURL; base != "" {
|
||||
if base := readAIABaseURL(caDir); base != "" {
|
||||
if issuingCA != "" {
|
||||
aiaURL = base + "/" + issuingCA + "/ca_cert.crt"
|
||||
} else {
|
||||
@@ -419,6 +361,7 @@ func makeCert(certDir, subjectName string, sans []string, caDir, issuingCA strin
|
||||
certOut := filepath.Join(certDir, certName+"_cert.pem")
|
||||
keyOut := filepath.Join(certDir, certName+"_key.pem")
|
||||
if fileExists(certOut) && fileExists(keyOut) {
|
||||
fmt.Printf("Certificate already exists in %s, skipping.\n", certDir)
|
||||
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 {
|
||||
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)
|
||||
@@ -488,7 +431,7 @@ func makePFX(certPath, caDir, issuingCA, password string, appleOpenSSL bool) err
|
||||
return errors.New("server certificate or key not found")
|
||||
}
|
||||
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
|
||||
@@ -502,16 +445,14 @@ func makePFX(certPath, caDir, issuingCA, password string, appleOpenSSL bool) err
|
||||
if password == "" {
|
||||
password = "changeit"
|
||||
}
|
||||
|
||||
if fileExists(pfxPath) {
|
||||
fmt.Println("PKCS#12 (PFX) file already exists, aborting generation.")
|
||||
return errors.New("PFX file already exists")
|
||||
return errors.New("PKCS#12 (PFX) file already exists, aborting generation")
|
||||
}
|
||||
|
||||
fmt.Print("Generating PKCS#12 (PFX) file...")
|
||||
|
||||
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)
|
||||
@@ -546,7 +487,7 @@ func makePFX(certPath, caDir, issuingCA, password string, appleOpenSSL bool) err
|
||||
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")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -575,7 +516,7 @@ func makePFXViaAppleOpenSSL(certPath, keyPath, rootCertPath, issuingCACertPath,
|
||||
}
|
||||
chainFile.Close()
|
||||
|
||||
cmd := exec.Command("/usr/bin/openssl", "pkcs12",
|
||||
cmd := exec.Command(opensslBin, "pkcs12",
|
||||
"-export", "-out", pfxPath,
|
||||
"-inkey", keyPath,
|
||||
"-in", certPath,
|
||||
@@ -585,7 +526,7 @@ func makePFXViaAppleOpenSSL(certPath, keyPath, rootCertPath, issuingCACertPath,
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
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.")
|
||||
return nil
|
||||
@@ -616,102 +557,56 @@ func newRootCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
func newMakeCACmd() *cobra.Command {
|
||||
var (
|
||||
days int
|
||||
issuingCA string
|
||||
aiaBaseURL string
|
||||
caDir string
|
||||
)
|
||||
var days int
|
||||
var issuingCA, aiaBaseURL, caDir string
|
||||
cmd := &cobra.Command{
|
||||
Use: "make-ca CA_NAME",
|
||||
Short: "Create a root or issuing CA.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
dir := resolveCADir(caDir)
|
||||
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)
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return makeCA(resolveCADir(caDir), args[0], days, issuingCA, aiaBaseURL)
|
||||
},
|
||||
}
|
||||
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(&aiaBaseURL, "aia-base-url", "", "base URL for the AIA caIssuers extension")
|
||||
cmd.Flags().StringVar(&caDir, "ca-dir", "", "directory for CA files")
|
||||
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "create an issuing CA with this directory name")
|
||||
cmd.Flags().StringVar(&aiaBaseURL, "aia-base-url", "", "base URL for AIA caIssuers extension")
|
||||
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA root directory")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newMakeCertCmd() *cobra.Command {
|
||||
var (
|
||||
certDir string
|
||||
caDir string
|
||||
issuingCA string
|
||||
days int
|
||||
)
|
||||
var certDir, caDir, issuingCA string
|
||||
var days int
|
||||
cmd := &cobra.Command{
|
||||
Use: "make-cert SUBJECT [SAN...]",
|
||||
Short: "Create a server/client certificate signed by the CA.",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
dir := resolveCADir(caDir)
|
||||
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)
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return makeCert(args[0], args[1:], resolveCADir(caDir), certDir, issuingCA, days)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&certDir, "cert-dir", "", "directory to store the certificate files")
|
||||
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA directory")
|
||||
cmd.Flags().StringVar(&certDir, "cert-dir", "", "output directory (default: signing CA directory)")
|
||||
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA root directory")
|
||||
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name")
|
||||
cmd.Flags().IntVar(&days, "days", 365, "validity period in days")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newMakePFXCmd() *cobra.Command {
|
||||
var (
|
||||
caDir string
|
||||
issuingCA string
|
||||
password string
|
||||
appleOpenSSL bool
|
||||
)
|
||||
var caDir, issuingCA, password string
|
||||
var appleOpenSSL bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "make-pfx CERT_PATH",
|
||||
Short: "Create a PKCS#12 (PFX) bundle for a leaf certificate.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
dir := resolveCADir(caDir)
|
||||
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)
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return makePFX(args[0], resolveCADir(caDir), issuingCA, 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(&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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
def dirs(tmp_path):
|
||||
ca = tmp_path / "ca"
|
||||
certs = tmp_path / "certs"
|
||||
ca.mkdir()
|
||||
certs.mkdir()
|
||||
return ca, certs
|
||||
return ca
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -46,7 +44,7 @@ def dirs(tmp_path):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_standalone_ca(dirs):
|
||||
ca, certs = dirs
|
||||
ca = dirs
|
||||
py("make-ca", "--ca-dir", str(ca), "Test CA")
|
||||
|
||||
assert (ca / "ca_cert.pem").exists()
|
||||
@@ -54,11 +52,10 @@ def test_standalone_ca(dirs):
|
||||
assert (ca / "simple-ca.json").exists()
|
||||
verify_cert(ca / "ca_cert.pem", ca / "ca_bundle.pem")
|
||||
|
||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
||||
"test", "test.example.com", "127.0.0.1")
|
||||
py("make-cert", "--ca-dir", str(ca), "test", "test.example.com", "127.0.0.1")
|
||||
|
||||
assert (certs / "test_cert.pem").exists()
|
||||
verify_cert(certs / "test_cert.pem", ca / "ca_bundle.pem")
|
||||
assert (ca / "test_cert.pem").exists()
|
||||
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):
|
||||
ca, certs = dirs
|
||||
ca = dirs
|
||||
py("make-ca", "--ca-dir", str(ca), "Test Root CA")
|
||||
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
|
||||
|
||||
assert (ca / "issuing_ca" / "ca_cert.pem").exists()
|
||||
verify_cert(ca / "issuing_ca" / "ca_cert.pem", ca / "ca_bundle.pem")
|
||||
|
||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
||||
"--issuing-ca", "issuing_ca", "test", "test.example.com", "127.0.0.1")
|
||||
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||
"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",
|
||||
"--password", "s3cr3t", str(certs / "test_cert.pem"))
|
||||
"--password", "s3cr3t", str(ca / "issuing_ca" / "test_cert.pem"))
|
||||
|
||||
assert (certs / "test.pfx").exists()
|
||||
result = openssl("pkcs12", "-in", str(certs / "test.pfx"), "-noout", "-info",
|
||||
assert (ca / "issuing_ca" / "test.pfx").exists()
|
||||
result = openssl("pkcs12", "-in", str(ca / "issuing_ca" / "test.pfx"), "-noout", "-info",
|
||||
"-password", "pass:s3cr3t")
|
||||
assert result.returncode == 0
|
||||
|
||||
@@ -92,31 +89,31 @@ def test_two_level_ca(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), "--issuing-ca", "issuing_ca", "Issuing CA")
|
||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
||||
"--issuing-ca", "issuing_ca", "test", "test.example.com", "127.0.0.1")
|
||||
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||
"test", "test.example.com", "127.0.0.1")
|
||||
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")
|
||||
assert "PBES2" in (info.stdout + info.stderr), "Expected modern PBES2 encryption"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS only")
|
||||
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), "--issuing-ca", "issuing_ca", "Issuing CA")
|
||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
||||
"--issuing-ca", "issuing_ca", "test", "test.example.com", "127.0.0.1")
|
||||
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||
"test", "test.example.com", "127.0.0.1")
|
||||
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(
|
||||
["/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"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
@@ -130,23 +127,24 @@ def test_pfx_apple_openssl(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), "--issuing-ca", "issuing_ca", "Issuing CA")
|
||||
|
||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
||||
"--issuing-ca", "issuing_ca", "alice", "alice.example.com")
|
||||
py("make-cert", "--ca-dir", str(ca), "--cert-dir", str(certs),
|
||||
"--issuing-ca", "issuing_ca", "bob", "bob.example.com")
|
||||
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||
"alice", "alice.example.com")
|
||||
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
|
||||
"bob", "bob.example.com")
|
||||
|
||||
alice_serial = cert_serial(certs / "alice_cert.pem")
|
||||
bob_serial = cert_serial(certs / "bob_cert.pem")
|
||||
issuing_dir = ca / "issuing_ca"
|
||||
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",
|
||||
str(certs / "alice_cert.pem"))
|
||||
str(issuing_dir / "alice_cert.pem"))
|
||||
|
||||
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()
|
||||
|
||||
crl_text = openssl("crl", "-in", str(issuing_crl), "-noout", "-text").stdout.upper()
|
||||
|
||||
Reference in New Issue
Block a user