Files
tessera-ctl/.planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh
T
schalli 76a30458fc docs(quick-261009-ikt): Cert-Manager-Umbau
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 17:12:10 +02:00

923 lines
48 KiB
Bash
Executable File

#!/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 <ausgabe> <datei[:name]>... — schickt Fixtures (oder absolute Pfade) als multipart, gibt den HTTP-Status aus.
# analyze_pw <passwords-json> <ausgabe> <datei[:name]>... — 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 <ausgabe-json> <content> <cert> <include-root> <pool-datei>... — 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 <anfrage-json-datei> <ausgabe> — 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 <antwort-json> — 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 <ausgabe-json> <json> — 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 <ordner> <json> — 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 <erwarteter-status> <erwarteter-code> <json> — 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 <ausgabe> <json-body> — 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 2>/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("<I", data, at + 24, 0) # deklarierte Groesse im Zentralverzeichnis
at = data.find(sig, at + 4)
open(path, "wb").write(data)
PY
[ "$(stat -c %s "$E2E_TMP/bomb.zip")" -lt 1000000 ] || e2e_fail "CR-01: Bomben-ZIP ist nicht klein"
secs=$(curl -s -o "$out" -w '%{time_total}' -b "$JAR" -F "files=@$E2E_TMP/bomb.zip;filename=bomb.zip" "$API/modules/cert-manager/analyze")
python3 -I - "$out" "$secs" <<'PY' || e2e_fail "CR-01: ZIP-Bombe nicht abgewiesen"
import json, sys
d = json.load(open(sys.argv[1]))
assert d["items"] == [], d
assert [(i["path"], i["reason"]) for i in d["ignored"]] == [("bomb.zip/bombe.pem", "tooLarge")], d["ignored"]
assert float(sys.argv[2]) < 5, f"zu langsam: {sys.argv[2]}"
PY
curl -s -o /dev/null -w '%{http_code}' "$API/health" | grep -q 200 || e2e_fail "CR-01: API nach der ZIP-Bombe nicht gesund"
# CR-02: 5 MiB lauter BEGIN-Zeilen ohne END
python3 -I -c 'import sys; open(sys.argv[1],"w").write("-----BEGIN CERTIFICATE-----\n"*185000)' "$E2E_TMP/begins.pem"
secs=$(curl -s -o "$out" -w '%{time_total}' -b "$JAR" -F "files=@$E2E_TMP/begins.pem;filename=begins.pem" "$API/modules/cert-manager/analyze")
python3 -I - "$out" "$secs" <<'PY' || e2e_fail "CR-02: BEGIN-Zeilen nicht schnell verarbeitet"
import json, sys
d = json.load(open(sys.argv[1]))
assert d["items"] == [], d
assert float(sys.argv[2]) < 2, f"zu langsam: {sys.argv[2]}"
PY
# CR-03: Umlaut- und Euro-Passwoerter, Hin- und Rueckweg mit openssl
local pw cert key enc
for pw in 'pässwörd' 'pw€'; do
for enc in compat modern; do
dir="$E2E_TMP/umlaut-$enc-${#pw}"
build_files "$dir" "{\"content\":\"pfx\",\"certPem\":\"@rsa-leaf.pem\",\"poolPems\":[\"@rsa-inter.pem\"],\"keyPem\":\"@rsa-leaf-key.pem\",\"password\":\"$pw\",\"pfxEncryption\":\"$enc\"}"
openssl pkcs12 -in "$dir/www.example.test.pfx" -passin "pass:$pw" -noout \
|| e2e_fail "CR-03: openssl liest $enc-PFX mit Passwort $pw nicht"
openssl pkcs12 -in "$dir/www.example.test.pfx" -passin "pass:falsch" -noout 2>/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 "$@"