From c04341043650947b3edc00cd417d2b99b55e80e4 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Fri, 24 Apr 2026 23:25:44 +0200 Subject: [PATCH] Update: replace hash symlink with CA bundle for certificate verification --- README.md | 2 +- run-tests.sh | 6 +++--- simple-ca.py | 47 +++++++++++++++++++---------------------------- simple-ca.sh | 32 ++++++++++++++++---------------- 4 files changed, 39 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index f2db23c..235895a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ make_ca [--days ] [--issuing-ca ] - `--days `: Optional. The number of days the CA certificate will be valid. Default is 3650 days (10 years). - `--issuing-ca `: Optional. If specified, creates an intermediate CA with as the intermediate CA name and using as certificate and key file prefix for the issuing CA (instead of root's `ca`). -It also creates a *hash link* symbolic link for the CA certificate, which is required by OpenSSL when using the `verify` command with `-CApath` option. +It also maintains a `ca_bundle.pem` file in the CA directory containing the root CA and any issuing CA certificates concatenated together. Use this bundle with `openssl verify -CAfile /ca_bundle.pem` instead of relying on hash symlinks — this works identically on Linux, macOS, and Windows without symlink privileges. ### `make_cert()` diff --git a/run-tests.sh b/run-tests.sh index e7a6f0f..74bb959 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -46,10 +46,10 @@ function display_certificate() { openssl x509 -in "$CERT_PATH" -noout -subject -issuer -serial -fingerprint echo - echo -e "\nVerifying certificate against the CA ($CA_DIR)..." + echo -e "\nVerifying certificate against the CA bundle ($CA_DIR/ca_bundle.pem)..." - # Verify the certificate against the CA - if openssl verify -CApath "$CA_DIR" "$CERT_PATH" 2>/dev/null; then + # Verify the certificate against the CA bundle + if openssl verify -CAfile "$CA_DIR/ca_bundle.pem" "$CERT_PATH" 2>/dev/null; then echo "Certificate verification successful." else echo "ERROR: Certificate verification failed." >&2 diff --git a/simple-ca.py b/simple-ca.py index ed393a4..2cc2fa1 100644 --- a/simple-ca.py +++ b/simple-ca.py @@ -33,32 +33,23 @@ def _err(msg): print(f"ERROR: {msg}", file=sys.stderr) -def make_hash_link(cert_path): - if not os.path.isfile(cert_path): - _err(f"Certificate file {cert_path} does not exist.") - return False - - cert_dir = os.path.dirname(cert_path) - try: - result = subprocess.run( - ["openssl", "x509", "-in", cert_path, "-noout", "-hash"], - capture_output=True, text=True, check=True, - ) - except subprocess.CalledProcessError: - _err(f"Failed to calculate hash for certificate {cert_path}.") - return False - - cert_hash = result.stdout.strip() - if not cert_hash: - _err(f"Failed to calculate hash for certificate {cert_path}.") - return False - - link_path = os.path.join(cert_dir, f"{cert_hash}.0") - target = os.path.basename(cert_path) - if os.path.islink(link_path) or os.path.exists(link_path): - os.remove(link_path) - os.symlink(target, link_path) - return True +def _rebuild_ca_bundle(ca_dir): + """Write ca_bundle.pem = root cert + any issuing CA certs in this dir.""" + bundle_path = os.path.join(ca_dir, "ca_bundle.pem") + parts = [] + root = os.path.join(ca_dir, "ca_cert.pem") + if os.path.isfile(root): + with open(root, "rb") as f: + parts.append(f.read()) + for name in sorted(os.listdir(ca_dir)): + if name == "ca_cert.pem" or not name.endswith("_cert.pem"): + continue + path = os.path.join(ca_dir, name) + if os.path.isfile(path): + with open(path, "rb") as f: + parts.append(f.read()) + with open(bundle_path, "wb") as f: + f.write(b"".join(parts)) def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None): @@ -119,7 +110,7 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None): _err("Failed to generate CA certificate and key.") return False - make_hash_link(root_ca_cert_path) + _rebuild_ca_bundle(ca_dir) if aia_base_url: with open(aia_file, "w") as f: @@ -157,7 +148,7 @@ def make_ca(ca_dir, ca_name, days=3650, issuing_ca=None, aia_base_url=None): _err("Failed to generate issuing CA certificate and key.") return False - make_hash_link(ca_cert_path) + _rebuild_ca_bundle(ca_dir) if aia_base_url: with open(aia_file, "w") as f: diff --git a/simple-ca.sh b/simple-ca.sh index cd26b63..d7b449b 100755 --- a/simple-ca.sh +++ b/simple-ca.sh @@ -22,19 +22,19 @@ # These functions require Bash and OpenSSL to be installed on the system. -function make_hash_link() { - local CERT_PATH="$1" - if [[ ! -f "$CERT_PATH" ]]; then - echo "ERROR: Certificate file $CERT_PATH does not exist." >&2 - return 1 +function _rebuild_ca_bundle() { + local CA_DIR="$1" + local BUNDLE="$CA_DIR/ca_bundle.pem" + : > "$BUNDLE" + if [[ -f "$CA_DIR/ca_cert.pem" ]]; then + cat "$CA_DIR/ca_cert.pem" >> "$BUNDLE" fi - local CERT_DIR="$(dirname "$CERT_PATH")" - local HASH="$(openssl x509 -in "$CERT_PATH" -noout -hash 2>/dev/null)" - if [[ -z "$HASH" ]]; then - echo "ERROR: Failed to calculate hash for certificate $CERT_PATH." >&2 - return 1 - fi - ln -sf "$(basename "$CERT_PATH")" "$CERT_DIR/${HASH}.0" + local f + for f in "$CA_DIR"/*_cert.pem; do + [[ -f "$f" ]] || continue + [[ "$(basename "$f")" == "ca_cert.pem" ]] && continue + cat "$f" >> "$BUNDLE" + done } function make_ca() { @@ -129,8 +129,8 @@ function make_ca() { return 1 fi - # Make a "hash" symlink for the CA certificate to allow OpenSSL to find it when verifying other certificates - make_hash_link "$CA_DIR/$ROOT_CA_CERT" + # Rebuild the CA bundle (root + any issuing CAs) for use with `openssl verify -CAfile` + _rebuild_ca_bundle "$CA_DIR" if [[ -n "$AIA_BASE_URL" ]]; then echo "$AIA_BASE_URL" > "$CA_DIR/aia_base_url.txt" @@ -162,8 +162,8 @@ function make_ca() { fi fi - # Make a "hash" symlink for the issuing CA certificate to allow OpenSSL to find it when verifying other certificates - make_hash_link "$CA_DIR/${CA_CERT}" + # Rebuild the CA bundle (root + any issuing CAs) for use with `openssl verify -CAfile` + _rebuild_ca_bundle "$CA_DIR" if [[ -n "$AIA_BASE_URL" ]]; then echo "$AIA_BASE_URL" > "$CA_DIR/aia_base_url.txt"