#!/usr/bin/env bash # Live-Pruefung des Zertifikat-Managers (quick-261009-ikt) gegen die lokale API. # Nur Testwerte aus den Fixtures; liest keine .env-Dateien. # Aufruf: e2e-cert.sh [files|fullchain|zip|inputs|formats|templates|version|aia|review|all] # all = jeder bisher gebaute Abschnitt in dieser Reihenfolge (Task 1: files, Task 2: fullchain, Task 3: zip, Task 4: inputs, Task 5: formats, Task 6: templates und version, Task 7: aia). # Voraussetzung: Stack laeuft und die API wurde neu gebaut (docker compose up -d --build api web). set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=/dev/null source "$HERE/../../261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/e2e-lib.sh" ROOT="$(git -C "$HERE" rev-parse --show-toplevel)" FIX="$ROOT/apps/api/src/cert-manager/__fixtures__" JAR="$E2E_TMP/cert.jar" BUILT="files fullchain zip inputs formats templates version aia review" # Abschnitte, die schon gebaut sind (jeder Task haengt seinen an) setup() { e2e_wait_health e2e_login "$JAR" e2e_activate "$JAR" cert-manager local code code=$(e2e_status "$JAR" POST "$API/modules/cert-manager/parse" '{}') if [ "$code" != "404" ]; then e2e_fail "API-Container neu bauen: docker compose up -d --build api (parse antwortet noch mit $code)" fi } # analyze ... — schickt Fixtures (oder absolute Pfade) als multipart, gibt den HTTP-Status aus. # analyze_pw ... — wie analyze, mit dem Feld `passwords` (nur fuer diesen Aufruf). analyze_pw() { local ANALYZE_PASSWORDS=$1; shift; analyze "$@"; } # Mit gesetztem (lokalem) ANALYZE_PASSWORDS wird das Feld `passwords` mitgeschickt. analyze() { local out=$1; shift local args=() spec path name [ -z "${ANALYZE_PASSWORDS:-}" ] || args+=(-F "passwords=$ANALYZE_PASSWORDS") for spec in "$@"; do path="${spec%%:*}"; name="${spec#*:}" case "$path" in /*) ;; *) path="$FIX/$path" ;; esac args+=(-F "files=@$path;filename=$name") done curl -s -o "$out" -w '%{http_code}' -b "$JAR" "${args[@]}" "$API/modules/cert-manager/analyze" } section_files() { local out="$E2E_TMP/analyze.out" code code=$(analyze "$out" rsa-leaf.pem:rsa-leaf.pem rsa-inter.pem:rsa-inter.pem ec-leaf.cer:ec-leaf.cer \ ec-inter.pem:ec-inter.pem ec-root.pem:ec-root.pem rsa-leaf.cer:rsa-leaf.cer) e2e_expect 200 "$code" "analyze mit sechs Dateien" python3 -I - "$out" <<'PY' || e2e_fail "analyze-Antwort stimmt nicht" import json, sys d = json.load(open(sys.argv[1])) certs = [i for i in d["items"] if i["kind"] == "certificate"] assert len(certs) == 5, f"fuenf verschiedene Zertifikate erwartet, waren {len(certs)}" by_cn = {c["cn"]: c for c in certs} leaf = by_cn["www.example.test"] assert [s["file"] for s in leaf["sources"]] == [0, 5], leaf["sources"] ec = by_cn["ec.example.test"] assert ec["keyType"] == "EC" and ec["curve"] == "P-256", (ec["keyType"], ec["curve"]) assert ec["role"] == "end-entity" assert by_cn["Tessera Test Inter RSA"]["role"] == "intermediate" assert by_cn["Tessera Test Inter EC"]["role"] == "intermediate" assert by_cn["Tessera Test Root EC"]["role"] == "root" assert d["locked"] == [] and d["ignored"] == [] # Task 2: zwei Ketten, RSA unvollstaendig (Luecke nach der CA), EC vollstaendig mit Wurzel assert len(d["chains"]) == 2, d["chains"] by_id = {c["id"]: c for c in certs} rsa = [c for c in d["chains"] if by_id[c["headId"]]["cn"] == "www.example.test"][0] ec_chain = [c for c in d["chains"] if by_id[c["headId"]]["cn"] == "ec.example.test"][0] assert rsa["complete"] is False and rsa["gap"]["kind"] == "afterCa", rsa assert rsa["gap"]["missingIssuerCn"] == "Tessera Test Root RSA" assert ec_chain["complete"] is True and by_id[ec_chain["rootId"]]["cn"] == "Tessera Test Root EC" PY code=$(curl -s -o "$out" -w '%{http_code}' -b "$JAR" -X POST "$API/modules/cert-manager/analyze") e2e_expect 400 "$code" "analyze ohne Dateien" e2e_contains "$out" 'invalidInput' "analyze ohne Dateien: Code" code=$(analyze "$out" README.md:readme.txt) e2e_expect 200 "$code" "analyze mit unbekannter Datei" e2e_contains "$out" '"reason":"unknown"' "unbekannte Datei gemeldet" local route for route in parse split merge convert export; do code=$(e2e_status "$JAR" POST "$API/modules/cert-manager/$route" '{}') e2e_expect 404 "$code" "alte Route $route" done echo "e2e cert files ok" } # build_json ... — schreibt den Anfragekoerper. build_json() { local out=$1 content=$2 cert=$3 root=$4; shift 4 python3 -I - "$out" "$content" "$FIX/$cert" "$root" "$@" <<'PY' import json, sys out, content, cert, root = sys.argv[1:5] pool = [open(f"{__import__('os').path.dirname(cert)}/{n}").read() for n in sys.argv[5:]] body = {"content": content, "format": "pem", "certPem": open(cert).read(), "poolPems": pool, "includeRoot": root == "true"} json.dump(body, open(out, "w")) PY } # post_build — gibt den HTTP-Status aus. post_build() { curl -s -o "$2" -w '%{http_code}' -b "$JAR" -X POST -H 'Content-Type: application/json' \ --data-binary "@$1" "$API/modules/cert-manager/build" } # pem_subjects — schreibt die Zertifikate der ersten Datei als cert-N.pem nach $E2E_TMP # und gibt die CNs zeilenweise aus. pem_subjects() { python3 -I - "$1" "$E2E_TMP" <<'PY' import base64, json, re, sys d = json.load(open(sys.argv[1])) text = base64.b64decode(d["files"][0]["content"]).decode() blocks = re.findall(r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----\n", text, re.S) for i, b in enumerate(blocks): open(f"{sys.argv[2]}/cert-{i}.pem", "w").write(b) print(len(blocks)) PY } section_fullchain() { local out="$E2E_TMP/build.out" req="$E2E_TMP/build.req" code n # Pruefung, dass der API-Container diesen Task traegt: build existiert (leerer Koerper -> 400, nicht 404) code=$(e2e_status "$JAR" POST "$API/modules/cert-manager/build" '{}' "$out") [ "$code" != "404" ] || e2e_fail "API-Container neu bauen: docker compose up -d --build api (build fehlt)" e2e_expect 400 "$code" "build mit leerem Koerper" # Fullchain: EC-Server, Pool in falscher Reihenfolge plus fremdes RSA-Zwischenzertifikat build_json "$req" fullchain ec-leaf.pem false ec-root.pem ec-inter.pem rsa-inter.pem code=$(post_build "$req" "$out") e2e_expect 200 "$code" "Fullchain ohne Wurzel" n=$(pem_subjects "$out") [ "$n" = "2" ] || e2e_fail "Fullchain ohne Wurzel: zwei Zertifikate erwartet, waren $n" openssl x509 -in "$E2E_TMP/cert-0.pem" -noout -subject | grep -q 'ec.example.test' \ || e2e_fail "Fullchain: erstes Zertifikat ist nicht der Server" openssl verify -CAfile "$FIX/ec-root.pem" -untrusted "$E2E_TMP/cert-1.pem" "$E2E_TMP/cert-0.pem" \ | grep -q ': OK' || e2e_fail "openssl verify der Fullchain schlug fehl" python3 -I - "$out" <<'PY' || e2e_fail "Fullchain-Antwort stimmt nicht" import json, sys d = json.load(open(sys.argv[1])) assert d["files"][0]["filename"] == "ec.example.test-fullchain.pem", d["files"][0]["filename"] assert d["chainComplete"] is True and d["missingIssuerCn"] is None PY build_json "$req" fullchain ec-leaf.pem true ec-root.pem ec-inter.pem rsa-inter.pem code=$(post_build "$req" "$out") e2e_expect 200 "$code" "Fullchain mit Wurzel" n=$(pem_subjects "$out") [ "$n" = "3" ] || e2e_fail "Fullchain mit Wurzel: drei Zertifikate erwartet, waren $n" openssl x509 -in "$E2E_TMP/cert-2.pem" -noout -subject | grep -q 'Root EC' \ || e2e_fail "Fullchain mit Wurzel: Wurzel nicht zuletzt" build_json "$req" chain ec-leaf.pem false ec-root.pem ec-inter.pem rsa-inter.pem code=$(post_build "$req" "$out") e2e_expect 200 "$code" "Nur Kette" n=$(pem_subjects "$out") [ "$n" = "1" ] || e2e_fail "Nur Kette: ein Zertifikat erwartet, waren $n" openssl x509 -in "$E2E_TMP/cert-0.pem" -noout -subject | grep -q 'Inter EC' \ || e2e_fail "Nur Kette: nicht das Zwischenzertifikat" # RSA ohne Wurzel: unvollstaendig, Name des fehlenden Ausstellers build_json "$req" fullchain rsa-leaf.pem false rsa-inter.pem code=$(post_build "$req" "$out") e2e_expect 200 "$code" "RSA-Fullchain ohne Wurzel" python3 -I - "$out" <<'PY' || e2e_fail "RSA-Fullchain: Luecke nicht gemeldet" import json, sys d = json.load(open(sys.argv[1])) assert d["chainComplete"] is False assert d["missingIssuerCn"] == "Tessera Test Root RSA", d["missingIssuerCn"] PY # Fehlercodes build_json "$req" chain rsa-leaf.pem false code=$(post_build "$req" "$out") e2e_expect 400 "$code" "Nur Kette ohne Zwischenzertifikat" e2e_contains "$out" '"code":"noChain"' "noChain" python3 -I - "$req" <<'PY' import json, sys p = sys.argv[1] d = json.load(open(p)); d["certPem"] = "kein Zertifikat"; json.dump(d, open(p, "w")) PY code=$(post_build "$req" "$out") e2e_expect 400 "$code" "certPem kein Zertifikat" e2e_contains "$out" '"code":"notACertificate"' "notACertificate" # Grenze: ein Koerper innerhalb der DTO-Grenzen (rund 330 kB) wird gelesen, nie mit 413 abgewiesen python3 -I - "$req" "$FIX/ec-leaf.pem" <<'PY' import json, sys pool = ["\n".join(["A" * 64] * 255)[:16384] for _ in range(20)] json.dump({"content": "fullchain", "format": "pem", "certPem": open(sys.argv[2]).read(), "poolPems": pool}, open(sys.argv[1], "w")) PY [ "$(wc -c < "$req")" -gt 300000 ] || e2e_fail "Pruefkoerper zu klein" code=$(post_build "$req" "$out") e2e_expect 400 "$code" "Koerper innerhalb der DTO-Grenzen" e2e_contains "$out" '"code":"notACertificate"' "Koerper innerhalb der Grenzen: Code" # Ein Koerper ueber 512 KiB: 413 mit Code python3 -I - "$req" <<'PY' import json, sys json.dump({"content": "fullchain", "baseName": "x" * (600 * 1024)}, open(sys.argv[1], "w")) PY code=$(post_build "$req" "$out") e2e_expect 413 "$code" "Koerper ueber 512 KiB" e2e_contains "$out" '"code":"tooLarge"' "tooLarge" # Kaputtes JSON: 400 mit Code printf '{"content": ' > "$req" code=$(post_build "$req" "$out") e2e_expect 400 "$code" "kaputtes JSON" e2e_contains "$out" '"code":"invalidInput"' "invalidInput bei kaputtem JSON" # Alle anderen Routen behalten 100 kB (Anmeldung mit 150 kB -> 413), Nests JSON-Leser bleibt aktiv python3 -I - "$req" <<'PY' import json, sys json.dump({"username": "admin", "password": "x" * 150000}, open(sys.argv[1], "w")) PY code=$(curl -s -o "$out" -w '%{http_code}' -X POST -H 'Content-Type: application/json' \ --data-binary "@$req" "$API/auth/login") e2e_expect 413 "$code" "Anmeldung mit 150 kB" e2e_login "$E2E_TMP/cert-relogin.jar" echo "e2e cert fullchain ok" } # Abschnitt zip (Task 3): Hersteller-ZIP, PKCS#7 und die daraus gebaute Fullchain section_zip() { local out="$E2E_TMP/zip.out" out2="$E2E_TMP/zip2.out" req="$E2E_TMP/zip.req" code local vendor="$E2E_TMP/vendor.zip" python3 -I - "$FIX" "$vendor" <<'PY' import io, sys, zipfile fix, out = sys.argv[1], sys.argv[2] rd = lambda n: open(f"{fix}/{n}", "rb").read() inner = io.BytesIO() with zipfile.ZipFile(inner, "w") as z: z.writestr("x.txt", "innen") with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z: z.writestr("ServerCertificate.crt", rd("ec-leaf.pem")) z.writestr("Intermediate/CA.crt", rd("ec-inter.pem")) z.writestr("Root/root.crt", rd("ec-root.pem")) z.writestr("__MACOSX/._ServerCertificate.crt", b"mac") z.writestr("inner.zip", inner.getvalue()) z.writestr("readme.txt", "Bitte lesen") PY code=$(analyze "$out" rsa-leaf.pem:rsa-leaf.pem rsa-inter.pem:rsa-inter.pem "$vendor:vendor.zip") e2e_expect 200 "$code" "analyze mit Hersteller-ZIP" python3 -I - "$out" <<'PY' || e2e_fail "ZIP-Analyse stimmt nicht" import json, sys raw = open(sys.argv[1]).read() assert "MACOSX" not in raw, "MACOSX darf nirgends vorkommen" d = json.loads(raw) certs = [i for i in d["items"] if i["kind"] == "certificate"] assert len(certs) == 5, f"fuenf verschiedene Zertifikate erwartet, waren {len(certs)}" assert len(d["chains"]) == 2, d["chains"] reasons = {(i["path"], i["reason"]) for i in d["ignored"]} assert ("vendor.zip/inner.zip", "nestedZip") in reasons, reasons assert ("vendor.zip/readme.txt", "unknown") in reasons, reasons ec = [c for c in certs if c["cn"] == "ec.example.test"][0] assert ec["sources"] == [{"file": 2, "path": "vendor.zip/ServerCertificate.crt"}], ec["sources"] PY # dasselbe ZIP ohne die Endung .zip: erkannt an den Anfangsbytes code=$(analyze "$out2" "$vendor:bundle.dat") e2e_expect 200 "$code" "analyze mit ZIP als bundle.dat" python3 -I - "$out2" <<'PY' || e2e_fail "ZIP als bundle.dat: Zertifikate stimmen nicht" import json, sys d = json.load(open(sys.argv[1])) cns = sorted(i["cn"] for i in d["items"] if i["kind"] == "certificate") assert cns == ["Tessera Test Inter EC", "Tessera Test Root EC", "ec.example.test"], cns assert any(i["path"].startswith("bundle.dat/") for i in d["ignored"]), d["ignored"] PY # PKCS#7: DER (RSA) und PEM (EC, von forge allein nicht lesbar), je drei Zertifikate local p7 n for p7 in rsa-chain.p7c ec-chain.p7b; do code=$(analyze "$out" "$p7:$p7") e2e_expect 200 "$code" "analyze $p7" n=$(python3 -I -c 'import json,sys; print(sum(1 for i in json.load(open(sys.argv[1]))["items"] if i["kind"]=="certificate"))' "$out") [ "$n" = "3" ] || e2e_fail "$p7: drei Zertifikate erwartet, waren $n" done # Fullchain aus den EC-Zertifikaten, die aus dem ZIP erkannt wurden: zwei Bloecke, openssl prueft die Kette python3 -I - "$out2" "$req" <<'PY' import json, sys d = json.load(open(sys.argv[1])) by = {i["cn"]: i["pem"] for i in d["items"] if i["kind"] == "certificate"} json.dump({"content": "fullchain", "format": "pem", "certPem": by["ec.example.test"], "poolPems": [by["Tessera Test Root EC"], by["Tessera Test Inter EC"]], "includeRoot": False}, open(sys.argv[2], "w")) PY code=$(post_build "$req" "$out") e2e_expect 200 "$code" "Fullchain aus ZIP-Zertifikaten" n=$(pem_subjects "$out") [ "$n" = "2" ] || e2e_fail "Fullchain aus ZIP: zwei Bloecke erwartet, waren $n" openssl verify -CAfile "$FIX/ec-root.pem" -untrusted "$E2E_TMP/cert-1.pem" "$E2E_TMP/cert-0.pem" \ | grep -q ': OK' || e2e_fail "openssl verify der Fullchain aus dem ZIP schlug fehl" echo "e2e cert zip ok" } # Abschnitt inputs (Task 4): Schluessel, PFX und CSR mit Passwort je Datei, Zuordnung, kein Passwort im Log section_inputs() { local out="$E2E_TMP/inputs.out" code local pw='Test-Pass-123' # Vollstaendiger Satz mit Passwoertern im Takt der Dateien code=$(analyze_pw "[\"\",\"\",\"\",\"$pw\",\"\",\"$pw\",\"\"]" "$out" rsa-leaf.pem:rsa-leaf.pem rsa-inter.pem:rsa-inter.pem rsa-root.pem:rsa-root.pem \ rsa-leaf-key-enc-trad.pem:rsa-leaf-key-enc-trad.pem rsa-leaf.csr:rsa-leaf.csr \ ec-compat.pfx:ec-compat.pfx ec-leaf.csr.der:ec-leaf.csr.der) e2e_expect 200 "$code" "analyze mit Schluessel, PFX und CSR" python3 -I - "$out" <<'PY' || e2e_fail "inputs: Analyse stimmt nicht" import json, sys raw = open(sys.argv[1]).read() assert "Test-Pass-123" not in raw, "Passwort in der Antwort" d = json.loads(raw) by_kind = {} for i in d["items"]: by_kind.setdefault(i["kind"], []).append(i) assert len(by_kind["certificate"]) == 6, len(by_kind["certificate"]) assert sorted(k["keyType"] for k in by_kind["privateKey"]) == ["EC", "RSA"] assert sorted(r["keyType"] for r in by_kind["csr"]) == ["EC", "RSA"] assert d["locked"] == [], d["locked"] certs = {c["cn"]: c for c in by_kind["certificate"]} keys = {k["keyType"]: k for k in by_kind["privateKey"]} csrs = {r["keyType"]: r for r in by_kind["csr"]} rsa, ec = certs["www.example.test"], certs["ec.example.test"] assert rsa["keyId"] == keys["RSA"]["id"] and keys["RSA"]["certIds"] == [rsa["id"]] assert keys["RSA"]["wasEncrypted"] is True assert rsa["csrIds"] == [csrs["RSA"]["id"]] assert csrs["RSA"]["keyId"] == keys["RSA"]["id"] and csrs["RSA"]["certIds"] == [rsa["id"]] assert ec["keyId"] == keys["EC"]["id"], "EC-Schluessel aus der PFX fehlt am EC-Zertifikat" assert csrs["EC"]["certIds"] == [ec["id"]] and csrs["EC"]["keyId"] == keys["EC"]["id"] assert "PRIVATE KEY" in keys["RSA"]["pem"] # nur als PKCS#8-Antwort fuer die Oberflaeche, nie im Log PY # PFX ohne Passwort gesperrt, mit falschem Passwort passwordWrong code=$(analyze "$out" rsa-modern.pfx:rsa-modern.pfx) e2e_expect 200 "$code" "analyze mit gesperrter PFX" e2e_contains "$out" '"reason":"passwordNeeded"' "PFX ohne Passwort" e2e_contains "$out" '"container":"pkcs12"' "Container pkcs12" code=$(analyze_pw '["falsch"]' "$out" rsa-modern.pfx:rsa-modern.pfx) e2e_expect 200 "$code" "analyze mit falschem Passwort" e2e_contains "$out" '"reason":"passwordWrong"' "PFX mit falschem Passwort" # Altes RC2-Format und PFX ohne Endung, mit Passwort: Zertifikate plus Schluessel local f for f in rsa-legacy.pfx rsa-modern.bin; do code=$(analyze_pw "[\"$pw\"]" "$out" "$f:$f") e2e_expect 200 "$code" "analyze $f" python3 -I - "$out" "$f" <<'PY' || e2e_fail "$f: Zertifikate und Schluessel erwartet" import json, sys d = json.load(open(sys.argv[1])) kinds = [i["kind"] for i in d["items"]] assert kinds.count("certificate") == 3 and kinds.count("privateKey") == 1, (sys.argv[2], kinds) assert d["locked"] == [] PY done # Verschluesselter Schluessel: ohne Passwort gesperrt, mit falschem passwordWrong code=$(analyze "$out" ec-leaf-key-enc-pkcs8.der:ec-leaf-key-enc-pkcs8.der) e2e_contains "$out" '"container":"privateKey"' "verschluesselter Schluessel gesperrt" code=$(analyze_pw '["falsch"]' "$out" ec-leaf-key-enc-pkcs8.der:ec-leaf-key-enc-pkcs8.der) e2e_contains "$out" '"reason":"passwordWrong"' "verschluesselter Schluessel mit falschem Passwort" # Ungueltiges Feld passwords: 400 invalidInput code=$(analyze_pw 'nope' "$out" rsa-leaf.pem:rsa-leaf.pem) e2e_expect 400 "$code" "passwords kein JSON" e2e_contains "$out" '"code":"invalidInput"' "passwords: invalidInput" # Kein Passwort und kein Schluessel im API-Log local logs logs=$(cd "$ROOT" && docker compose logs api --since 10m 2>&1) if printf '%s' "$logs" | grep -q "$pw"; then e2e_fail "Passwort im API-Log"; fi if printf '%s' "$logs" | grep -q 'PRIVATE KEY'; then e2e_fail "Schluessel im API-Log"; fi echo "e2e cert inputs ok" } # mkbody — schreibt den Anfragekoerper; Werte "@datei" werden durch den Inhalt der Fixture ersetzt. mkbody() { python3 -I - "$1" "$2" "$FIX" <<'PY' import json, sys out, spec, fix = sys.argv[1:4] def sub(v): if isinstance(v, str) and v.startswith("@"): return open(f"{fix}/{v[1:]}").read() if isinstance(v, list): return [sub(x) for x in v] return v json.dump({k: sub(v) for k, v in json.loads(spec).items()}, open(out, "w")) PY } # build_files — schickt build (Status 200 erwartet) und legt alle Dateien der Antwort im Ordner ab. build_files() { local dir=$1 body=$2 code mkdir -p "$dir" mkbody "$dir/request.json" "$body" code=$(post_build "$dir/request.json" "$dir/response.json") e2e_expect 200 "$code" "build $(basename "$dir")" python3 -I - "$dir/response.json" "$dir" <<'PY' import base64, json, os, sys d = json.load(open(sys.argv[1])) for f in d["files"]: open(os.path.join(sys.argv[2], os.path.basename(f["filename"])), "wb").write(base64.b64decode(f["content"])) PY } # build_fails — build muss mit diesem Status und Code scheitern. build_fails() { local want=$1 wcode=$2 body=$3 code mkbody "$E2E_TMP/fail.req" "$body" code=$(post_build "$E2E_TMP/fail.req" "$E2E_TMP/fail.out") e2e_expect "$want" "$code" "build-Fehler $wcode" e2e_contains "$E2E_TMP/fail.out" "\"code\":\"$wcode\"" "build-Fehlercode $wcode" } # Abschnitt formats (Task 5): jede Ausgabe, von openssl gelesen section_formats() { local pfxpw='Neu-Pass-2026' keypw='Test-Pass-123' local set cn leaf key pool_inter pool_root inter_cn root_cn dir pool_json for set in rsa ec; do if [ "$set" = rsa ]; then cn='www.example.test'; inter_cn='Tessera Test Inter RSA'; root_cn='Tessera Test Root RSA' else cn='ec.example.test'; inter_cn='Tessera Test Inter EC'; root_cn='Tessera Test Root EC' fi leaf="$set-leaf.pem"; key="$set-leaf-key.pem" pool_json="[\"@$set-root.pem\",\"@$set-inter.pem\"]" # Einzelzertifikat als DER dir="$E2E_TMP/fmt-$set-leaf" build_files "$dir" "{\"content\":\"leaf\",\"format\":\"der\",\"certPem\":\"@$leaf\"}" openssl x509 -inform DER -in "$dir/$cn.cer" -noout -subject | grep -q "$cn" \ || e2e_fail "$set: DER-Zertifikat von openssl nicht lesbar" # Einzelzertifikat als PKCS#7 (nur das Serverzertifikat) build_files "$dir-p7b" "{\"content\":\"leaf\",\"format\":\"p7b\",\"certPem\":\"@$leaf\"}" [ "$(openssl pkcs7 -in "$dir-p7b/$cn.p7b" -print_certs | grep -c '^subject=')" = 1 ] \ || e2e_fail "$set: leaf p7b enthaelt nicht genau ein Zertifikat" # Fullchain als p7b und p7c mit Wurzel: openssl listet Server, Zwischen, Wurzel in dieser Reihenfolge dir="$E2E_TMP/fmt-$set-chain" build_files "$dir" "{\"content\":\"fullchain\",\"format\":\"p7b\",\"certPem\":\"@$leaf\",\"poolPems\":$pool_json,\"includeRoot\":true}" build_files "$dir-c" "{\"content\":\"fullchain\",\"format\":\"p7c\",\"certPem\":\"@$leaf\",\"poolPems\":$pool_json,\"includeRoot\":true}" local order_b order_c want order_b=$(openssl pkcs7 -in "$dir/$cn-fullchain.p7b" -print_certs | grep '^subject=' | sed -E 's/.*CN ?= ?//') order_c=$(openssl pkcs7 -inform DER -in "$dir-c/$cn-fullchain.p7c" -print_certs | grep '^subject=' | sed -E 's/.*CN ?= ?//') want=$(printf '%s\n%s\n%s' "$cn" "$inter_cn" "$root_cn") [ "$order_b" = "$want" ] || e2e_fail "$set: Fullchain p7b Reihenfolge falsch: $order_b" [ "$order_c" = "$want" ] || e2e_fail "$set: Fullchain p7c Reihenfolge falsch: $order_c" echo "--- $set Fullchain p7b/p7c (openssl pkcs7 -print_certs): $(printf '%s' "$order_b" | paste -sd ',' -) | $(printf '%s' "$order_c" | paste -sd ',' -)" # ohne Wurzel (Vorgabe) nur Server und Zwischenzertifikat; Nur Kette ohne Server build_files "$dir-n" "{\"content\":\"fullchain\",\"format\":\"p7b\",\"certPem\":\"@$leaf\",\"poolPems\":$pool_json}" [ "$(openssl pkcs7 -in "$dir-n/$cn-fullchain.p7b" -print_certs | grep -c '^subject=')" = 2 ] \ || e2e_fail "$set: Fullchain p7b ohne Wurzel sollte zwei Zertifikate haben" build_files "$dir-k" "{\"content\":\"chain\",\"format\":\"p7c\",\"certPem\":\"@$leaf\",\"poolPems\":$pool_json}" [ "$(openssl pkcs7 -inform DER -in "$dir-k/$cn-chain.p7c" -print_certs | grep '^subject=' | sed -E 's/.*CN ?= ?//')" = "$inter_cn" ] \ || e2e_fail "$set: Nur Kette p7c sollte nur das Zwischenzertifikat enthalten" # Zertifikat und Schluessel in einer PEM-Datei: erstes Zertifikat ist der Server, Schluessel passt dir="$E2E_TMP/fmt-$set-bundle" build_files "$dir" "{\"content\":\"leafKey\",\"format\":\"pem\",\"certPem\":\"@$leaf\",\"poolPems\":$pool_json,\"keyPem\":\"@$key\"}" openssl x509 -in "$dir/$cn-bundle.pem" -noout -subject | grep -q "$cn" \ || e2e_fail "$set: erstes Zertifikat im Bundle ist nicht der Server" [ "$(grep -c 'BEGIN CERTIFICATE' "$dir/$cn-bundle.pem")" = 2 ] || e2e_fail "$set: Bundle sollte zwei Zertifikate haben" [ "$(openssl pkey -in "$dir/$cn-bundle.pem" -pubout)" = "$(openssl x509 -in "$dir/$cn-bundle.pem" -pubkey -noout)" ] \ || e2e_fail "$set: Schluessel im Bundle passt nicht zum Zertifikat" # PFX in beiden Verschluesselungen local enc info for enc in compat modern; do dir="$E2E_TMP/fmt-$set-pfx-$enc" build_files "$dir" "{\"content\":\"pfx\",\"certPem\":\"@$leaf\",\"poolPems\":$pool_json,\"keyPem\":\"@$key\",\"password\":\"$pfxpw\",\"pfxEncryption\":\"$enc\"}" info=$(openssl pkcs12 -info -noout -passin "pass:$pfxpw" -in "$dir/$cn.pfx" 2>&1) \ || e2e_fail "$set $enc: openssl liest die PFX nicht: $info" if [ "$enc" = compat ]; then printf '%s' "$info" | grep -q 'pbeWithSHA1And3-KeyTripleDES-CBC' || e2e_fail "$set: kompatible PFX ohne 3DES: $info" printf '%s\n' "$info" > "$E2E_TMP/pfx-$set-compat.info" else printf '%s' "$info" | grep -q 'AES-256-CBC' || e2e_fail "$set: moderne PFX ohne AES-256-CBC: $info" printf '%s\n' "$info" > "$E2E_TMP/pfx-$set-modern.info" fi # Inhalt: Serverzertifikat und Schluessel gehoeren zusammen [ "$(openssl pkcs12 -passin "pass:$pfxpw" -in "$dir/$cn.pfx" -nocerts -nodes 2>/dev/null | openssl pkey -pubout)" \ = "$(openssl pkcs12 -passin "pass:$pfxpw" -in "$dir/$cn.pfx" -clcerts -nokeys 2>/dev/null | openssl x509 -pubkey -noout)" ] \ || e2e_fail "$set $enc: Schluessel und Zertifikat in der PFX passen nicht zusammen" openssl pkcs12 -passin "pass:$pfxpw" -in "$dir/$cn.pfx" -clcerts -nokeys 2>/dev/null | openssl x509 -noout -subject | grep -q "$cn" \ || e2e_fail "$set $enc: Serverzertifikat fehlt in der PFX" done # Schluessel: PKCS#8 mit Passwort, klassisch (mit und ohne), DER mit Passwort dir="$E2E_TMP/fmt-$set-key" build_files "$dir" "{\"content\":\"key\",\"format\":\"pkcs8\",\"keyPem\":\"@$key\",\"password\":\"$keypw\",\"baseName\":\"k\"}" openssl pkey -passin "pass:$keypw" -noout -in "$dir/k.key" || e2e_fail "$set: verschluesselter PKCS#8 nicht lesbar" head -1 "$dir/k.key" | grep -q 'BEGIN ENCRYPTED PRIVATE KEY' || e2e_fail "$set: PKCS#8 mit Passwort nicht verschluesselt" build_files "$dir-t" "{\"content\":\"key\",\"format\":\"traditional\",\"keyPem\":\"@$key\",\"baseName\":\"k\"}" if [ "$set" = rsa ]; then head -1 "$dir-t/k.rsa.key" | grep -q 'BEGIN RSA PRIVATE KEY' || e2e_fail "rsa: klassischer Schluessel ohne RSA-Kopf" openssl pkey -noout -in "$dir-t/k.rsa.key" || e2e_fail "rsa: klassischer Schluessel nicht lesbar" else head -1 "$dir-t/k.ec.key" | grep -q 'BEGIN EC PRIVATE KEY' || e2e_fail "ec: klassischer Schluessel ohne EC-Kopf" openssl pkey -noout -in "$dir-t/k.ec.key" || e2e_fail "ec: klassischer Schluessel nicht lesbar" fi build_files "$dir-d" "{\"content\":\"key\",\"format\":\"pkcs8-der\",\"keyPem\":\"@$key\",\"password\":\"$keypw\",\"baseName\":\"k\"}" openssl pkey -inform DER -passin "pass:$keypw" -noout -in "$dir-d/k.key.der" || e2e_fail "$set: DER-Schluessel mit Passwort nicht lesbar" # CSR als DER und als PEM dir="$E2E_TMP/fmt-$set-csr" build_files "$dir" "{\"content\":\"csr\",\"format\":\"der\",\"csrPem\":\"@$set-leaf.csr\",\"baseName\":\"anfrage\"}" openssl req -inform DER -noout -subject -in "$dir/anfrage.csr.der" | grep -q "$cn" || e2e_fail "$set: CSR (DER) nicht lesbar" build_files "$dir-p" "{\"content\":\"csr\",\"format\":\"pem\",\"csrPem\":\"@$set-leaf.csr\",\"baseName\":\"anfrage\"}" openssl req -noout -subject -in "$dir-p/anfrage.csr" | grep -q "$cn" || e2e_fail "$set: CSR (PEM) nicht lesbar" done # Fehlerfaelle mit Code build_fails 400 passwordRequired '{"content":"pfx","certPem":"@rsa-leaf.pem","keyPem":"@rsa-leaf-key.pem"}' build_fails 400 keyMismatch '{"content":"leafKey","certPem":"@rsa-leaf.pem","keyPem":"@ec-leaf-key.pem"}' build_fails 400 keyMissing '{"content":"leafKey","certPem":"@rsa-leaf.pem"}' build_fails 400 invalidInput '{"content":"csr","format":"p7b","csrPem":"@rsa-leaf.csr"}' # Kein Passwort und kein Schluessel im API-Log local logs logs=$(cd "$ROOT" && docker compose logs api --since 10m 2>&1) local secret for secret in 'Test-Pass-123' 'Neu-Pass-2026' 'PRIVATE KEY'; do if printf '%s' "$logs" | grep -q "$secret"; then e2e_fail "$secret im API-Log"; fi done # Belege fuer die Zusammenfassung: PFX-Algorithmen und Reihenfolge der PKCS#7-Dateien echo "--- openssl pkcs12 -info (rsa, kompatibel):"; grep -E 'MAC:|Shrouded' "$E2E_TMP/pfx-rsa-compat.info" echo "--- openssl pkcs12 -info (rsa, modern):"; grep -E 'MAC:|Shrouded' "$E2E_TMP/pfx-rsa-modern.info" echo "e2e cert formats ok" } # Abschnitt templates (Task 6): jede Vorlage fuer RSA und EC, von openssl gelesen section_templates() { local pfxpw='Neu-Pass-2026' local set cn leaf key dir pool_json for set in rsa ec; do if [ "$set" = rsa ]; then cn='www.example.test'; else cn='ec.example.test'; fi leaf="$set-leaf.pem"; key="$set-leaf-key.pem" pool_json="[\"@$set-root.pem\",\"@$set-inter.pem\"]" local base="\"content\":\"template\",\"certPem\":\"@$leaf\",\"poolPems\":$pool_json,\"keyPem\":\"@$key\"" local leafpub; leafpub=$(openssl x509 -in "$FIX/$leaf" -pubkey -noout) # Nginx und Apache: Fullchain besteht die Pruefung gegen die Wurzel, der Schluessel passt zum Zertifikat local tpl for tpl in nginx apache; do dir="$E2E_TMP/tpl-$set-$tpl" build_files "$dir" "{$base,\"template\":\"$tpl\"}" openssl verify -CAfile "$FIX/$set-root.pem" -untrusted "$dir/fullchain.pem" "$FIX/$leaf" >/dev/null \ || e2e_fail "$set $tpl: openssl verify der Kette" [ "$(grep -c 'BEGIN CERTIFICATE' "$dir/fullchain.pem")" = 2 ] || e2e_fail "$set $tpl: fullchain ohne Wurzel sollte zwei Zertifikate haben" [ "$(openssl pkey -in "$dir/privkey.pem" -pubout)" = "$leafpub" ] || e2e_fail "$set $tpl: Schluessel passt nicht zum Zertifikat" head -1 "$dir/privkey.pem" | grep -q 'BEGIN PRIVATE KEY' || e2e_fail "$set $tpl: privkey.pem nicht PKCS#8" done python3 -I -c "import json,sys; d=json.load(open(sys.argv[1])); assert 'ssl_certificate /etc/nginx/ssl/'+sys.argv[2]+'/fullchain.pem;' in d['snippet'], d['snippet']" "$E2E_TMP/tpl-$set-nginx/response.json" "$cn" \ || e2e_fail "$set nginx: Schnipsel falsch" # Apache aelter als 2.4.8: Zertifikat, Kette und Schluessel getrennt dir="$E2E_TMP/tpl-$set-apache-legacy" build_files "$dir" "{$base,\"template\":\"apache-legacy\"}" openssl verify -CAfile "$FIX/$set-root.pem" -untrusted "$dir/chain.pem" "$dir/cert.pem" >/dev/null \ || e2e_fail "$set apache-legacy: openssl verify cert.pem mit chain.pem" [ "$(grep -c 'BEGIN CERTIFICATE' "$dir/cert.pem")" = 1 ] || e2e_fail "$set apache-legacy: cert.pem sollte nur das Serverzertifikat enthalten" [ "$(grep -c 'BEGIN CERTIFICATE' "$dir/chain.pem")" = 1 ] || e2e_fail "$set apache-legacy: chain.pem sollte nur das Zwischenzertifikat enthalten" [ "$(openssl pkey -in "$dir/privkey.pem" -pubout)" = "$leafpub" ] || e2e_fail "$set apache-legacy: Schluessel passt nicht" # IIS: PFX mit 3DES (ohne pfxEncryption in der Anfrage), Schluessel und Zertifikat gehoeren zusammen dir="$E2E_TMP/tpl-$set-iis" build_files "$dir" "{$base,\"template\":\"iis\",\"password\":\"$pfxpw\"}" openssl pkcs12 -info -noout -passin "pass:$pfxpw" -in "$dir/$cn.pfx" 2>&1 | grep -q 'pbeWithSHA1And3-KeyTripleDES-CBC' \ || e2e_fail "$set iis: PFX ohne 3DES-Schluesselbeutel" [ "$(openssl pkcs12 -passin "pass:$pfxpw" -in "$dir/$cn.pfx" -nocerts -nodes 2>/dev/null | openssl pkey -pubout)" = "$leafpub" ] \ || e2e_fail "$set iis: Schluessel in der PFX passt nicht zum Zertifikat" echo "--- $set iis (openssl pkcs12 -info): $(openssl pkcs12 -info -noout -passin "pass:$pfxpw" -in "$dir/$cn.pfx" 2>&1 | grep -E 'Shrouded' | tr -s ' ')" # Nginx Proxy Manager: Schluessel im klassischen Format, Zertifikat und Zwischenzertifikat getrennt, kein Schnipsel dir="$E2E_TMP/tpl-$set-npm" build_files "$dir" "{$base,\"template\":\"npm\"}" if [ "$set" = rsa ]; then want='BEGIN RSA PRIVATE KEY'; else want='BEGIN EC PRIVATE KEY'; fi head -1 "$dir/privkey.pem" | grep -q "$want" || e2e_fail "$set npm: privkey.pem ohne $want" openssl pkey -in "$dir/privkey.pem" -noout || e2e_fail "$set npm: privkey.pem nicht lesbar" [ "$(grep -c 'BEGIN CERTIFICATE' "$dir/certificate.pem")" = 1 ] || e2e_fail "$set npm: certificate.pem sollte nur das Serverzertifikat enthalten" [ "$(grep -c 'BEGIN CERTIFICATE' "$dir/intermediate.pem")" = 1 ] || e2e_fail "$set npm: intermediate.pem sollte nur das Zwischenzertifikat enthalten" python3 -I -c "import json,sys; assert json.load(open(sys.argv[1]))['snippet'] is None" "$dir/response.json" \ || e2e_fail "$set npm: Schnipsel sollte leer sein" # HAProxy: eine Datei, erstes Zertifikat ist der Server, Schluessel am Ende dir="$E2E_TMP/tpl-$set-haproxy" build_files "$dir" "{$base,\"template\":\"haproxy\"}" openssl x509 -in "$dir/$cn.pem" -noout -subject | grep -q "$cn" || e2e_fail "$set haproxy: erstes Zertifikat ist nicht der Server" [ "$(grep -c 'BEGIN CERTIFICATE' "$dir/$cn.pem")" = 2 ] || e2e_fail "$set haproxy: sollte zwei Zertifikate haben" [ "$(openssl pkey -in "$dir/$cn.pem" -pubout)" = "$leafpub" ] || e2e_fail "$set haproxy: Schluessel passt nicht" [ "$(grep -n 'BEGIN PRIVATE KEY' "$dir/$cn.pem" | cut -d: -f1)" -gt "$(grep -n 'END CERTIFICATE' "$dir/$cn.pem" | tail -1 | cut -d: -f1)" ] \ || e2e_fail "$set haproxy: Schluessel steht nicht hinter den Zertifikaten" # Tomcat: PKCS#12 mit dem Passwort lesbar, Schnipsel mit IHR-PASSWORT und ohne das echte Passwort dir="$E2E_TMP/tpl-$set-tomcat" build_files "$dir" "{$base,\"template\":\"tomcat\",\"password\":\"$pfxpw\"}" openssl pkcs12 -info -noout -passin "pass:$pfxpw" -in "$dir/$cn.p12" >/dev/null 2>&1 || e2e_fail "$set tomcat: p12 nicht lesbar" python3 -I - "$dir/response.json" "$pfxpw" "$cn" <<'PY' || e2e_fail "$set tomcat: Schnipsel falsch" import json, sys d = json.load(open(sys.argv[1])) assert "IHR-PASSWORT" in d["snippet"] and sys.argv[2] not in d["snippet"], d["snippet"] assert f'certificateKeyAlias="{sys.argv[3]}"' in d["snippet"], d["snippet"] PY done # Wurzel nur auf Wunsch: nginx mit includeRoot hat drei Zertifikate dir="$E2E_TMP/tpl-root" build_files "$dir" "{\"content\":\"template\",\"template\":\"nginx\",\"includeRoot\":true,\"certPem\":\"@rsa-leaf.pem\",\"poolPems\":[\"@rsa-root.pem\",\"@rsa-inter.pem\"],\"keyPem\":\"@rsa-leaf-key.pem\"}" [ "$(grep -c 'BEGIN CERTIFICATE' "$dir/fullchain.pem")" = 3 ] || e2e_fail "nginx mit Wurzel sollte drei Zertifikate haben" # Fehlerfaelle build_fails 400 templateNeedsKey '{"content":"template","template":"nginx","certPem":"@rsa-leaf.pem"}' build_fails 400 keyMismatch '{"content":"template","template":"nginx","certPem":"@rsa-leaf.pem","keyPem":"@ec-leaf-key.pem"}' build_fails 400 passwordRequired '{"content":"template","template":"iis","certPem":"@rsa-leaf.pem","keyPem":"@rsa-leaf-key.pem"}' build_fails 400 passwordRequired '{"content":"template","template":"tomcat","certPem":"@rsa-leaf.pem","keyPem":"@rsa-leaf-key.pem"}' mkbody "$E2E_TMP/fail.req" '{"content":"template","template":"weblogic","certPem":"@rsa-leaf.pem","keyPem":"@rsa-leaf-key.pem"}' e2e_expect 400 "$(post_build "$E2E_TMP/fail.req" "$E2E_TMP/fail.out")" "unbekannte Vorlage" # Kein Passwort und kein Schluessel im API-Log local logs secret logs=$(cd "$ROOT" && docker compose logs api --since 10m 2>&1) for secret in 'Neu-Pass-2026' 'PRIVATE KEY'; do if printf '%s' "$logs" | grep -q "$secret"; then e2e_fail "$secret im API-Log"; fi done echo "e2e cert templates ok" } # Abschnitt version (Task 6): Modul-Changelog und Katalog section_version() { local out="$E2E_TMP/changelog.out" code code=$(e2e_status "$JAR" GET "$API/modules/changelog/cert-manager" '' "$out") e2e_expect 200 "$code" "Modul-Changelog" python3 -I - "$out" <<'PY' || e2e_fail "Modul-Changelog stimmt nicht" import json, sys d = json.load(open(sys.argv[1])) first = d[0] assert first["version"] == "1.2.0", first["version"] assert first["date"] == "2026-10-09", first["date"] assert len(first["changes"]) >= 5, len(first["changes"]) assert d[1]["version"] == "1.1.0", d[1]["version"] assert any("Fehlendes Zertifikat holen" in c["de"] for c in first["changes"]), "AIA-Eintrag fehlt" PY code=$(e2e_status "$JAR" GET "$API/modules/catalog" '' "$out") e2e_expect 200 "$code" "Katalog" python3 -I - "$out" <<'PY' || e2e_fail "Katalog zeigt nicht Version 1.2.0" import json, sys d = json.load(open(sys.argv[1])) mod = [m for m in d if m.get("slug") == "cert-manager"] assert len(mod) == 1, "cert-manager fehlt im Katalog" assert mod[0]["version"] == "1.2.0", mod[0]["version"] PY echo "e2e cert version ok" } # post_fetch — schickt fetch-issuer, gibt den HTTP-Status aus. post_fetch() { curl -s -o "$1" -w '%{http_code}' -b "$JAR" -H 'Content-Type: application/json' --data-binary "@$2" \ "$API/modules/cert-manager/fetch-issuer" } # Abschnitt aia (Task 7): „Fehlendes Zertifikat holen“. Live gegen letsencrypt.org, ausser CERT_E2E_OFFLINE=1. section_aia() { local out="$E2E_TMP/fetch.out" req="$E2E_TMP/fetch.req" code # interne Adressen im Zertifikat (127.0.0.1, 169.254.169.254, ldap://): nie angefragt mkbody "$req" '{"pem":"@aia-private-leaf.pem"}' code=$(post_fetch "$out" "$req") e2e_expect 422 "$code" "fetch-issuer mit internen Adressen" e2e_contains "$out" '"code":"aiaInternal"' "fetch-issuer interne Adressen: Code" # Zertifikat ohne Aussteller-Adresse mkbody "$req" '{"pem":"@rsa-leaf-noaki.pem"}' code=$(post_fetch "$out" "$req") e2e_expect 422 "$code" "fetch-issuer ohne Adresse" e2e_contains "$out" '"code":"aiaMissing"' "fetch-issuer ohne Adresse: Code" # keine Zertifikatsdaten mkbody "$req" '{"pem":"das ist kein Zertifikat"}' code=$(post_fetch "$out" "$req") e2e_expect 400 "$code" "fetch-issuer mit Text" e2e_contains "$out" '"code":"notACertificate"' "fetch-issuer mit Text: Code" # eine vom Browser mitgeschickte Adresse wird ignoriert: gleiches Ergebnis wie ohne mkbody "$req" '{"pem":"@aia-private-leaf.pem","url":"http://127.0.0.1/"}' code=$(post_fetch "$out" "$req") e2e_expect 422 "$code" "fetch-issuer mit mitgeschickter Adresse" e2e_contains "$out" '"code":"aiaInternal"' "fetch-issuer mit mitgeschickter Adresse: wie ohne" # zu langer Text python3 -I - "$req" <<'PY' import json, sys json.dump({"pem": "x" * 16385}, open(sys.argv[1], "w")) PY code=$(post_fetch "$out" "$req") e2e_expect 400 "$code" "fetch-issuer mit zu langem Text" # ohne Anmeldung kein Zugriff code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' --data-binary "@$req" \ "$API/modules/cert-manager/fetch-issuer") e2e_expect 401 "$code" "fetch-issuer ohne Anmeldung" if [ "${CERT_E2E_OFFLINE:-}" = "1" ]; then echo "e2e cert aia: Live-Teil uebersprungen (CERT_E2E_OFFLINE=1)" else local leaf="$E2E_TMP/le-leaf.pem" fetched="$E2E_TMP/le-issuer.pem" openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org /dev/null \ | openssl x509 -outform PEM > "$leaf" || e2e_fail "Zertifikat von letsencrypt.org nicht lesbar (CERT_E2E_OFFLINE=1 setzen, wenn kein Netz)" [ -s "$leaf" ] || e2e_fail "leeres Zertifikat von letsencrypt.org" # 1. Das Blatt allein: Luecke direkt dahinter, mit http-Adresse code=$(analyze "$out" "$leaf:le-leaf.pem") e2e_expect 200 "$code" "analyze letsencrypt.org-Blatt" python3 -I - "$out" <<'PY' || e2e_fail "Luecke hinter dem Blatt stimmt nicht" import json, sys d = json.load(open(sys.argv[1])) assert len(d["chains"]) == 1, d["chains"] gap = d["chains"][0]["gap"] assert gap and gap["kind"] == "afterLeaf", gap assert gap["aiaUrls"] and gap["aiaUrls"][0].startswith("http"), gap PY # 2. Auf Knopfdruck holen python3 -I - "$leaf" "$req" <<'PY' import json, sys json.dump({"pem": open(sys.argv[1]).read()}, open(sys.argv[2], "w")) PY code=$(post_fetch "$out" "$req") e2e_expect 200 "$code" "fetch-issuer live" python3 -I - "$out" "$fetched" <<'PY' || e2e_fail "fetch-issuer-Antwort stimmt nicht" import json, sys d = json.load(open(sys.argv[1])) assert d["host"].endswith("lencr.org"), d["host"] assert d["pem"].count("BEGIN CERTIFICATE") == 1, d["pem"].count("BEGIN CERTIFICATE") assert d["cn"] and d["filename"].endswith(".crt"), (d["cn"], d["filename"]) open(sys.argv[2], "w").write(d["pem"]) print(f" live geholt: Server {d['host']}, Name {d['cn']}, Datei {d['filename']}") PY openssl verify -partial_chain -trusted "$fetched" "$leaf" >/dev/null \ || e2e_fail "openssl verify: geholtes Zertifikat hat das Blatt nicht ausgestellt" # 3. Blatt plus geholtes Zertifikat: Weg aus zwei Zertifikaten, naechste Luecke dahinter code=$(analyze "$out" "$leaf:le-leaf.pem" "$fetched:le-issuer.pem") e2e_expect 200 "$code" "analyze Blatt plus Geholtes" python3 -I - "$out" <<'PY' || e2e_fail "Kette mit dem geholten Zertifikat stimmt nicht" import json, sys d = json.load(open(sys.argv[1])) assert len(d["chains"]) == 1, d["chains"] chain = d["chains"][0] assert len(chain["path"]) == 2, chain["path"] assert chain["gap"] and chain["gap"]["kind"] == "afterCa", chain["gap"] by_id = {i["id"]: i for i in d["items"]} print(f" naechste Stufe: Weg {[by_id[i]['cn'] for i in chain['path']]}, es fehlt \"{chain['gap']['missingIssuerCn']}\", Adressen {chain['gap']['aiaUrls']}") PY fi local logs logs=$(cd "$ROOT" && docker compose logs api --since 10m 2>&1) if printf '%s' "$logs" | grep -q 'BEGIN CERTIFICATE'; then e2e_fail "API-Log enthaelt Zertifikatstext" fi echo "e2e cert aia ok" } # Abschnitt review (Review-Korrekturen): CR-01 ZIP-Bombe, CR-02 PEM-Scanner, CR-03 Umlaut-Passwoerter, # WR-01 CORS, WR-04 Rechenaufwand, WR-05 Anzahl, WR-06 Gesamtgroesse, WR-07 Wurzel ohne basicConstraints section_review() { local out="$E2E_TMP/review.out" code secs dir # CR-01: 200 MiB Nullbytes, Kopfdaten behaupten Groesse 0 -> tooLarge, schnell, API lebt weiter python3 -I - "$E2E_TMP/bomb.zip" <<'PY' import struct, sys, zipfile path = sys.argv[1] with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z: z.writestr("bombe.pem", bytes(200 * 1024 * 1024)) data = bytearray(open(path, "rb").read()) sig = b"PK\x01\x02" at = data.find(sig) while at != -1: struct.pack_into("/dev/null \ && e2e_fail "CR-03: openssl oeffnet $enc-PFX mit falschem Passwort" done done for spec in "rsa-umlaut-modern.pfx:pässwörd" "rsa-umlaut-compat.pfx:pässwörd" "rsa-euro-modern.pfx:pw€" "rsa-euro-compat.pfx:pw€"; do pw="${spec#*:}"; local f="${spec%%:*}" code=$(analyze_pw "[\"$pw\"]" "$out" "$f:$f") e2e_expect 200 "$code" "CR-03 analyze $f" python3 -I - "$out" "$f" <<'PY' || e2e_fail "CR-03: $f mit richtigem Passwort nicht geoeffnet" import json, sys d = json.load(open(sys.argv[1])) assert d["locked"] == [], d["locked"] assert len([i for i in d["items"] if i["kind"] == "certificate"]) == 3, sys.argv[2] assert len([i for i in d["items"] if i["kind"] == "privateKey"]) == 1, sys.argv[2] PY code=$(analyze_pw '["pasword"]' "$out" "$f:$f") e2e_expect 200 "$code" "CR-03 analyze $f falsches Passwort" e2e_contains "$out" '"passwordWrong"\|"passwordNeeded"' "CR-03 $f bleibt gesperrt" done # WR-04: zu aufwendiger Passwortschutz wird uebersprungen secs=$(curl -s -o "$out" -w '%{time_total}' -b "$JAR" -F 'passwords=["Test-Pass-123"]' \ -F "files=@$FIX/rsa-expensive.pfx;filename=teuer.pfx" -F "files=@$FIX/rsa-leaf-key-enc-expensive.pem;filename=teuer.pem" \ "$API/modules/cert-manager/analyze") python3 -I - "$out" "$secs" <<'PY' || e2e_fail "WR-04: teurer Passwortschutz nicht uebersprungen" import json, sys d = json.load(open(sys.argv[1])) assert d["items"] == [] and d["locked"] == [], d assert sorted(i["reason"] for i in d["ignored"]) == ["protectionTooExpensive"] * 2, d["ignored"] assert float(sys.argv[2]) < 2, sys.argv[2] PY # WR-05: 201 verschiedene Zertifikate in einer Datei -> 413 tooManyItems local many="$E2E_TMP/many.pem" i : > "$many" openssl ecparam -name prime256v1 -genkey -noout -out "$E2E_TMP/many.key" for i in $(seq 1 201); do openssl req -x509 -new -key "$E2E_TMP/many.key" -subj "/CN=massen-$i.example.test" -days 30 -set_serial "$i" >> "$many" 2>/dev/null done code=$(analyze "$out" "$many:many.pem") e2e_expect 413 "$code" "WR-05 201 Zertifikate" e2e_contains "$out" '"code":"tooManyItems"' "WR-05 Code" head -n "$(( $(grep -c 'END CERTIFICATE' "$many") * 0 + $(grep -n 'END CERTIFICATE' "$many" | sed -n 200p | cut -d: -f1) ))" "$many" > "$E2E_TMP/many200.pem" code=$(analyze "$out" "$E2E_TMP/many200.pem:many200.pem") e2e_expect 200 "$code" "WR-05 200 Zertifikate" # WR-06: fuenf Dateien zu je 4,9 MiB (zusammen ueber 20 MiB, je Datei unter der Einzelgrenze) werden beim Empfang abgewiesen local j args=() for j in 1 2 3 4; do head -c $((49 * 1024 * 1024 / 10)) /dev/zero > "$E2E_TMP/gross-$j.bin" args+=(-F "files=@$E2E_TMP/gross-$j.bin;filename=gross-$j.bin") done args+=(-F "files=@$E2E_TMP/gross-1.bin;filename=gross-5.bin") secs=$(curl -s -o "$out" -w '%{http_code} %{time_total}' -b "$JAR" "${args[@]}" "$API/modules/cert-manager/analyze") case "$secs" in 413\ *) ;; *) e2e_fail "WR-06: erwartet 413, war $secs" ;; esac e2e_contains "$out" 'tooLarge' "WR-06 Code" # WR-01: zu grosser build-Koerper traegt CORS-Kopfzeilen python3 -I -c 'import json; print(json.dumps({"content": "x" * 600000}))' > "$E2E_TMP/bigbuild.json" curl -s -D "$E2E_TMP/cors.headers" -o "$out" -b "$JAR" -H 'Origin: http://localhost:3000' -H 'Content-Type: application/json' \ --data-binary "@$E2E_TMP/bigbuild.json" "$API/modules/cert-manager/build" head -1 "$E2E_TMP/cors.headers" | grep -q '413' || e2e_fail "WR-01: erwartet 413 ($(head -1 "$E2E_TMP/cors.headers"))" grep -qi '^access-control-allow-origin: http://localhost:3000' "$E2E_TMP/cors.headers" \ || { cat "$E2E_TMP/cors.headers" >&2; e2e_fail "WR-01: 413 ohne CORS-Kopfzeile"; } e2e_contains "$out" '"code":"tooLarge"' "WR-01 Code" # WR-07: Wurzel ohne basicConstraints code=$(analyze "$out" selfsigned-v1-root.pem:v1.pem selfsigned-nobc-ca.pem:nobc-ca.pem selfsigned-nobc-leaf.pem:nobc-leaf.pem) e2e_expect 200 "$code" "WR-07 analyze" python3 -I - "$out" <<'PY' || e2e_fail "WR-07: Rollen stimmen nicht" import json, sys d = json.load(open(sys.argv[1])) role = {i["cn"]: i["role"] for i in d["items"] if i["kind"] == "certificate"} assert role["Tessera Test V1 Root"] == "root", role assert role["Tessera Test NoBC CA"] == "root", role assert role["nobc.example.test"] == "end-entity", role PY echo "e2e cert review ok" } run_section() { case "$1" in files) section_files ;; fullchain) section_fullchain ;; zip) section_zip ;; inputs) section_inputs ;; formats) section_formats ;; templates) section_templates ;; version) section_version ;; aia) section_aia ;; review) section_review ;; *) e2e_fail "unbekannter Abschnitt: $1 (files|fullchain|zip|inputs|formats|templates|version|aia|review|all)" ;; esac } main() { local what=${1:-all} s setup if [ "$what" = "all" ]; then for s in $BUILT; do run_section "$s"; done else run_section "$what" fi } main "$@"