Refactor simple-ca: Remove JSON config and streamline AIA URL handling
/ test-shell (push) Successful in 11s
/ test-python (push) Successful in 25s
/ test-go (push) Successful in 41s

- 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:
2026-05-24 21:40:06 +02:00
parent 04d8dab9bc
commit 935167ca8c
7 changed files with 440 additions and 316 deletions
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+126
View File
@@ -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
View File
@@ -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()