Files
simple-ca/test_simple_ca.py
T
slawek 9c84e82672
test-go.yaml / test-go (push) Successful in 1m27s
test-python.yaml / test-python (push) Successful in 1m36s
feat: Bring Go and Python implementations to feature parity
Go:
- Read and write simple-ca.json shared with the Python implementation
- Add make-crl and revoke-cert commands and record certificate history
- Rename --aia-base-url to --ca-publish-base-url
- Add CRL Distribution Point extension to issuing CA and leaf
  certificates
- Take days and issuing_ca defaults from simple-ca.json
- Include email SANs in server certificates
- Reject IPv4 SANs with out-of-range octets
- Record existing issuing CAs and rebuild the CA bundle
- Print errors once

Python:
- Add make-cert --type server|user and email SANs
- Reject IPv4 SANs with out-of-range octets
- Skip make-ca when the root CA exists and no issuing CA is given
- Build ca_bundle.pem from issuing CA subdirectories
- Accept a bare filename in make-pfx and drop the root key requirement
- Honour explicit --days values and reject values below 1
- Persist and increment CRL numbers in simple-ca.json

Update README and add tests for the new behaviour.
2026-10-16 14:20:04 +02:00

218 lines
8.0 KiB
Python

#!/usr/bin/env python3
# MIT License
# Copyright (c) 2026 Sławomir Koszewski
import subprocess
import sys
from pathlib import Path
import pytest
SCRIPT = Path(__file__).parent / "simple-ca.py"
def py(*args, check=True):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
capture_output=True, text=True, check=check,
)
def openssl(*args, check=True):
return subprocess.run(["openssl", *args], capture_output=True, text=True, check=check)
def cert_serial(cert_path):
out = openssl("x509", "-in", str(cert_path), "-noout", "-serial").stdout
return out.strip().split("=", 1)[-1].upper()
def verify_cert(cert_path, bundle_path):
result = openssl("verify", "-CAfile", str(bundle_path), str(cert_path), check=False)
assert result.returncode == 0, f"Certificate verification failed:\n{result.stderr}"
@pytest.fixture
def dirs(tmp_path):
ca = tmp_path / "ca"
ca.mkdir()
return ca
# ---------------------------------------------------------------------------
# Standalone CA
# ---------------------------------------------------------------------------
def test_standalone_ca(dirs):
ca = dirs
py("make-ca", "--ca-dir", str(ca), "Test CA")
assert (ca / "ca_cert.pem").exists()
assert (ca / "ca_bundle.pem").exists()
assert (ca / "simple-ca.json").exists()
verify_cert(ca / "ca_cert.pem", ca / "ca_bundle.pem")
py("make-cert", "--ca-dir", str(ca), "test", "test.example.com", "127.0.0.1")
assert (ca / "test_cert.pem").exists()
verify_cert(ca / "test_cert.pem", ca / "ca_bundle.pem")
# ---------------------------------------------------------------------------
# Two-level CA
# ---------------------------------------------------------------------------
def test_two_level_ca(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), "--issuing-ca", "issuing_ca",
"test", "test.example.com", "127.0.0.1")
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(ca / "issuing_ca" / "test_cert.pem"))
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
# ---------------------------------------------------------------------------
# PFX algorithm variants
# ---------------------------------------------------------------------------
def test_pfx_modern(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), "--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(ca / "issuing_ca" / "test_cert.pem"))
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 = 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), "--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(ca / "issuing_ca" / "test_cert.pem"))
result = subprocess.run(
["/usr/bin/openssl", "pkcs12", "-in", str(ca / "issuing_ca" / "test.pfx"),
"-noout", "-info", "-password", "pass:s3cr3t"],
capture_output=True, text=True,
)
assert result.returncode == 0
combined = result.stdout + result.stderr
assert "PBES2" not in combined, "Expected Apple-compatible (non-PBES2) format"
# ---------------------------------------------------------------------------
# CRL and revocation
# ---------------------------------------------------------------------------
def test_crl(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), "--issuing-ca", "issuing_ca",
"alice", "alice.example.com")
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca",
"bob", "bob.example.com")
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(issuing_dir / "alice_cert.pem"))
py("make-crl", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca")
issuing_crl = issuing_dir / "crl.pem"
assert issuing_crl.exists()
crl_text = openssl("crl", "-in", str(issuing_crl), "-noout", "-text").stdout.upper()
assert alice_serial in crl_text, f"Alice's serial {alice_serial} not found in issuing CA CRL"
assert bob_serial not in crl_text, f"Bob's serial {bob_serial} unexpectedly found in issuing CA CRL"
py("make-crl", "--ca-dir", str(ca))
root_crl = ca / "crl.pem"
assert root_crl.exists()
root_crl_text = openssl("crl", "-in", str(root_crl), "-noout", "-text").stdout
assert "No Revoked Certificates" in root_crl_text, "Root CA CRL should be empty"
def test_crl_number_increments(dirs):
ca = dirs
py("make-ca", "--ca-dir", str(ca), "CRL Test CA")
numbers = []
for _ in range(2):
py("make-crl", "--ca-dir", str(ca))
numbers.append(openssl("crl", "-in", str(ca / "crl.pem"), "-noout", "-crlnumber").stdout.strip())
assert numbers == ["crlNumber=0x01", "crlNumber=0x02"], numbers
# ---------------------------------------------------------------------------
# User certificate and SAN handling
# ---------------------------------------------------------------------------
def test_user_cert(dirs):
ca = dirs
py("make-ca", "--ca-dir", str(ca), "Test CA")
py("make-ca", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "Issuing CA")
py("make-cert", "--ca-dir", str(ca), "--issuing-ca", "issuing_ca", "--type", "user",
"Alice Example", "alice@example.com")
cert = ca / "issuing_ca" / "Alice Example_cert.pem"
verify_cert(cert, ca / "ca_bundle.pem")
text = openssl("x509", "-in", str(cert), "-noout", "-text").stdout
for eku in ("TLS Web Client Authentication", "E-mail Protection", "Code Signing"):
assert eku in text, f"EKU {eku!r} not found"
assert "email:alice@example.com" in text
assert "TLS Web Server Authentication" not in text
def test_invalid_ip_san(dirs):
ca = dirs
py("make-ca", "--ca-dir", str(ca), "Test CA")
result = py("make-cert", "--ca-dir", str(ca), "test", "999.1.1.1", check=False)
assert result.returncode != 0
assert not (ca / "test_cert.pem").exists()
def test_make_ca_root_exists(dirs):
ca = dirs
py("make-ca", "--ca-dir", str(ca), "Test CA")
result = py("make-ca", "--ca-dir", str(ca), "Test CA", check=False)
assert result.returncode == 0
assert "Traceback" not in result.stderr
def test_pfx_bare_filename(dirs):
ca = dirs
py("make-ca", "--ca-dir", str(ca), "Test CA")
py("make-cert", "--ca-dir", str(ca), "test", "test.example.com")
result = subprocess.run(
[sys.executable, str(SCRIPT), "make-pfx", "--ca-dir", str(ca), "test_cert.pem"],
capture_output=True, text=True, cwd=ca,
)
assert result.returncode == 0, result.stderr
assert (ca / "test.pfx").exists()