diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml index cdc1e31..f8af549 100644 --- a/.gitea/workflows/test.yaml +++ b/.gitea/workflows/test.yaml @@ -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 diff --git a/run-tests.sh b/run-tests.sh index 7a3fcf8..7b4b225 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -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" diff --git a/simple-ca.py b/simple-ca.py index 39696d2..7486d94 100755 --- a/simple-ca.py +++ b/simple-ca.py @@ -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, diff --git a/simple-ca.sh b/simple-ca.sh index 4f1adc0..e122cc9 100755 --- a/simple-ca.sh +++ b/simple-ca.sh @@ -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" \ diff --git a/src/simple-ca/main.go b/src/simple-ca/main.go index 3d8eb42..d6f177e 100644 --- a/src/simple-ca/main.go +++ b/src/simple-ca/main.go @@ -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 } diff --git a/src/simple-ca/main_test.go b/src/simple-ca/main_test.go new file mode 100644 index 0000000..5347b11 --- /dev/null +++ b/src/simple-ca/main_test.go @@ -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) + } +} diff --git a/test_simple_ca.py b/test_simple_ca.py index 41cdcc2..6fdca20 100644 --- a/test_simple_ca.py +++ b/test_simple_ca.py @@ -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()