76a30458fc
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
923 lines
48 KiB
Bash
Executable File
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 "$@"
|