Files
tessera-ctl/.planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/261009-ikt-SUMMARY.md
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

62 KiB
Raw Blame History

phase, plan, status, quick_id, tasks_done
phase plan status quick_id tasks_done
quick-261009-ikt 01 complete 261009-ikt
1
2
3
4
5
6
7
8

Quick 261009-ikt: Zertifikat-Manager Umbau - Summary

Task 1

Commit: 75ea83f feat(cert-manager): Reiter „Dateien“ mit mehreren Dateien und ein Parser für RSA und EC – Durchstich (84 Dateien, 16 Löschungen alle beabsichtigt: 7 API, 9 Web). Nicht gepusht.

Gates (gemessen, Stack neu gebaut mit docker compose up -d --build api web)

Gate Ergebnis
api vitest src/cert-manager src/module-registry 10 Dateien, 255 Tests grün
web vitest modules/cert-manager src/messages 7 Dateien, 41 Tests grün (inkl. umlaut-guard)
tsc api / web ohne Fehler
biome lint beide Modulordner sauber
biome check eigene Dateien (api cert-manager, web components, page, actions, working-set(+test), use-cert-workspace, Seitentest, de/en.json) sauber (7 eigene Dateien mit --write formatiert, nie ganze Ordner außerhalb des Moduls)
check-cert-messages.cjs 15 messages ok 51
Löschungs- und grep-Gates (alte Dateien weg, keine forge-Zertifikatparser, keine alten Routen im Controller) grün
Fixtures: keine ignorierten Dateien, Pflichtdateien vorhanden grün
e2e-cert.sh all (Abschnitt files) e2e cert files ok
gesamte <automated>-Kette letzte Zeile task1 ok

Fixtures (Output-Punkt 1)

OpenSSL 3.5.7 (9 Jun 2026). Erzeugt mit __fixtures__/make-fixtures.sh (47 Dateien im Ordner inkl. Skript und README):

  • RSA-PKI: rsa-root, rsa-root2, rsa-inter, rsa-inter-cross, rsa-inter-expired, rsa-inter-decoy (gleicher Name UND gleiche SubjectKeyIdentifier wie rsa-inter, anderer Schlüssel: checkIssued true, verify false), rsa-leaf (.pem/.cer), rsa-leaf-noaki, rsa-fullchain, rsa-chain.p7b/.p7c, rsa-trusted
  • EC-PKI: ec-root (P-384), ec-inter (P-384), ec-leaf (P-256, .pem/.cer), ec-fullchain, ec-chain.p7b
  • Sonstige Zertifikate: selfsigned-leaf, aia-private-leaf (127.0.0.1, 169.254.169.254, ldap://)
  • Schlüssel RSA: rsa-leaf-key (PKCS#8), -pkcs1, -enc-pkcs8, -enc-trad, -pkcs8.der, -pkcs1.der
  • Schlüssel EC: ec-leaf-key (PKCS#8), -sec1, -enc-pkcs8 (.pem/.der, 3DES), -enc-trad, -sec1.der
  • CSR: rsa-leaf.csr/.csr.der, ec-leaf.csr/.csr.der
  • PFX: rsa-modern, rsa-compat, rsa-legacy, rsa-nopass (leeres Passwort), rsa-modern.bin, ec-modern, ec-compat
  • encrypted-entry.zip (ZipCrypto, Passwort Test-Pass-123)
  • Passwort aller geschützten Dateien: Test-Pass-123. CA-Schlüssel werden nach der Erzeugung gelöscht.

Abweichungen

  1. [Rule 1 - Bug] selfSigned-Berechnung. D-18 verlangt checkIssued(self) && verify(self). OpenSSLs checkIssued lehnt ein Zertifikat ab, dessen Schlüsselverwendung kein keyCertSign enthält; ein selbstsigniertes Serverzertifikat mit digitalSignature,keyEncipherment wäre dann nicht „selbstsigniert“ (die Kette würde fälschlich „Zwischenzertifikat fehlt“ melden). Umsetzung: verify(eigener Schlüssel) UND (checkIssued(self) ODER subject === issuer). Erfüllt weiterhin die Spec-Anforderung (selfsigned-leaf: selfSigned true, Rolle end-entity). Datei: cert-model.ts, Funktion isSelfSigned.
  2. Dateinamen aus multer. multer liest Dateinamen als Latin-1; repairFileName im Controller gewinnt UTF-8-Umlaute zurück (mit Spec). Nicht im Plan, rein für die Anzeige.
  3. Der Plan nennt certError(code, status, message); so umgesetzt. Keine CERT_ERROR_STATUS-Tabelle.
  4. cert-manager.module.ts: Providerliste entfernt, Kommentar angepasst (Seeding und Logzeile unverändert).
  5. Keine Stubs: detectZip, detectPkcs12, detectPkcs7, detectPrivateKey, detectCsr sind laut Plan benannte leere Stufen (Tasks 3 und 4), analyzeWorkingSet liefert chains/locked leer bis Task 2/4. Nichts davon erreicht die Oberfläche als Platzhaltertext.

Bedrohungsstatus

  • T-ikt-02 (Upload-Größe): multer 30 x 5 MiB, Gesamtsumme 20 MiB -> 413 tooLarge (Controller-Spec); Web lehnt vorab ab (30 Einträge, 5 MiB, 10 MiB gesamt).
  • T-ikt-03 (kaputte Eingaben): jede Stufe in try/catch, Spec mit Zufallsbytes, leerer Datei, abgeschnittenem DER, kaputtem Base64: nie ein Fehler, nur unknown.
  • T-ikt-08 (Schlüssel/Passwörter): Passwort-Feld im Zustand vorhanden, aber noch nirgends befüllt oder gesendet; kein Logger-Aufruf mit Inhalten.
  • T-ikt-11 (Zugriff): Controller-Spec prüft Klassenpfad, Modul-Slug und die exakte Handlerliste; alte Routen liefern live 404.
  • T-ikt-12/13: Namen nur zur Anzeige (Steuerzeichen entfernt, max. 255), React-Text ohne dangerouslySetInnerHTML.

Hinweise für Task 2

  • API: cert-types.ts enthält schon alle Typen (BuildInput, ChainInfo, CertErrorCode, certError(code, status, message)). cert-output.ts enthält nur safeBaseName; buildOutput kommt dazu. analyzeWorkingSet in cert-analyze.ts setzt chains: [] - dort buildChains einhängen.
  • CertItem.keyId ist die Schlüsselkennung k-<16 Hex von sha256(SPKI-DER)> (gleiche Form wie KeyItem.id/CsrItem.keyId); selfSigned und role kommen schon aus certItemFromDer. Hilfsfunktionen in cert-model.ts exportiert: describeKey, keyIdOf, sha256Hex, roleOf, leadingDerSequence.
  • Fixtures für Task 2: rsa-inter-cross (andere Wurzel rsa-root2), rsa-inter-expired, rsa-inter-decoy (gleicher Name und gleiche SKI, anderer Schlüssel: Pflichttest für checkIssued + verify), rsa-leaf-noaki, ec-*, selfsigned-leaf. Fixture-Zertifikate sind ab "jetzt" (2026-10-09) 100 Jahre gültig, rsa-inter-expired 2020 bis 2021.
  • Web: useCertWorkspace() liefert { entries, analysis, analysisIds, status, errorKey, addFiles, remove, clear, retry }; analysisIds[source.file] ist die Eintrags-Kennung zum Analysezeitpunkt (Antworten mit alter Reihenfolge bleiben korrekt zuordenbar). actions.ts hat certErrorKey(error) (Code, 413 ohne Code -> tooLarge, sonst generic) für certManager.errors.<key>. ROLE_STYLES ist aus components/FilesTab.tsx exportiert. page.tsx kennt TabId = 'files'; neue Reiter dort in Reihenfolge einfügen und EmptyWorkspace bei leerer Liste.
  • Messages: Namespace certManager ist neu strukturiert (tabs, roles, files, errors mit allen D-24-Codes); check-cert-messages.cjs <min> zählt jetzt 51 Schlüssel, Mindestanzahl in späteren Tasks erhöhen. Neue deutsche Wörter mit ae/oe/ue/ss in umlaut-dictionary.ts (UMLAUT_ALLOWLIST) eintragen, wenn der Guard sie meldet.
  • e2e-cert.sh: BUILT="files"; neue Abschnitte als Funktion section_<name> anlegen, in run_section freischalten und an BUILT hängen. Setup prüft, dass POST parse 404 liefert (Hinweis zum Neubau). Hilfsfunktion analyze <out> <fixture>:<anzeigename>....
  • Live-Befund: API-Image-Neubau dauert über 2 Minuten; docker compose up -d --build api web im Hintergrund starten.

Task 2

Commit: 30118a2 feat(cert-manager): Zusammenführen mit Fullchain und Nur Kette (24 Dateien, keine Löschungen, 10 neu). Nicht gepusht. Das e2e-Skript unter .planning ist wie vorgegeben nicht eingecheckt.

Gates (gemessen, Stack neu gebaut mit docker compose up -d --build api web)

Gate Ergebnis
api vitest src/cert-manager src/module-registry 12 Dateien, 284 Tests grün
web vitest modules/cert-manager src/messages 8 Dateien, 55 Tests grün (inkl. umlaut-guard)
tsc api / web ohne Fehler
biome lint beide Modulordner, biome check eigene Dateien (api cert-manager + main.ts, web components, page, actions, working-set, use-cert-workspace, Seitentest, de/en.json) sauber
check-cert-messages.cjs 25 messages ok 70
certBuildJsonBody in main.ts, keine forge-Zertifikatparser grün
e2e-cert.sh all e2e cert files ok, e2e cert fullchain ok
gesamte <automated>-Kette letzte Zeile task2 ok

Live bewiesen (Abschnitt fullchain): openssl verify der Fullchain OK, Pool in falscher Reihenfolge plus fremdes RSA-Zwischenzertifikat ergibt genau 2 Blöcke (3 mit Wurzel, Wurzel zuletzt), Nur Kette = nur Zwischenzertifikat, RSA ohne Wurzel meldet chainComplete false und „Tessera Test Root RSA“, Körper von rund 330 kB innerhalb der DTO-Grenzen -> 400 notACertificate (nie 413), 600 KiB -> 413 tooLarge, kaputtes JSON -> 400 invalidInput, Anmeldung mit 150 kB -> 413 (100-kB-Grenze der anderen Routen unverändert), danach Anmeldung weiter möglich (Nests globaler JSON-Leser aktiv).

Abweichungen

  1. [Rule 3 - Blocking] Neue Datei cert-names.ts. safeBaseName liegt jetzt dort (cert-output.ts exportiert es weiterhin). Grund: cert-output.ts braucht jetzt certItemFromDer aus cert-model.ts, und cert-model.ts importierte safeBaseName aus cert-output.ts; das wäre eine Importschleife. cert-model.ts importiert jetzt aus cert-names. Verhalten unverändert, die bestehenden safeBaseName-Specs laufen unverändert gegen cert-output.
  2. buildChains(certs, headIds?): zusätzlicher optionaler Parameter für vorgegebene Kettenköpfe (buildOutput baut genau die Kette des gesendeten Serverzertifikats, auch wenn der Kopf ein Zwischenzertifikat ist). Ohne Parameter exakt das D-18-Verhalten.
  3. Nur-Kette-Knopf in der Oberfläche ist deaktiviert (mit Hinweis), wenn es keinen Aussteller zum Mitnehmen gibt; die API antwortet in diesem Fall weiterhin mit 400 noChain.
  4. Knöpfe benutzen die Design-Klassen btn btn-primary / btn btn-secondary aus globals.css.

Bedrohungsstatus

  • Body-Grenze (D-26): eigener Leser certBuildJsonBody (Funktionsname nie jsonParser), Express aus der Kopie von @nestjs/platform-express, main.ts registriert vor app.listen; Spec rechnet die größte DTO-Anfrage aus den exportierten Konstanten (unter 512 KiB) und prüft 413/400-Abbildung.
  • Reihenfolge nie vom Browser: buildOutput baut immer neu über buildChains, Fremdzertifikate im Pool werden verworfen, Nicht-Zertifikate -> 400 notACertificate.
  • Keine Passwörter oder Schlüssel in diesem Task im Spiel; keine Logger-Aufrufe mit Anfrageinhalt.

Hinweise für Task 3

  • API: cert-model.ts Stufen detectZip und detectPkcs7 sind weiter leere Slots; PEM-Schleife in detectPem hat den Kommentar für PKCS7/CMS-Etiketten. buildChains und analyzeWorkingSet brauchen für ZIP/P7B keine Änderung (Ketten entstehen aus allen erkannten Zertifikaten). cert-names.ts ist der Ort von safeBaseName.
  • Web: page.tsx hat TabId = 'files' | 'merge'; neue Reiter dort in der D-23-Reihenfolge einfügen (analyze, split zwischen files und merge). Für alle Nicht-Dateien-Reiter ist EmptyWorkspace und die Zeile „Grundlage: …“ schon in page.tsx verdrahtet (workspace.basis/workspace.empty/workspace.goToFiles); nur noch die Reiterinhalte ergänzen. ChainView und ROLE_STYLES (aus FilesTab) sind wiederverwendbar.
  • Messages: Namespace hat jetzt tabs.merge, workspace.*, merge.*, chain.*; check-cert-messages.cjs zählt 70 Schlüssel (Mindestzahl dort erhöhen). Umlaut-Allowlist: vertrauen ergänzt.
  • e2e-cert.sh: BUILT="files fullchain"; Hilfsfunktionen build_json <out> <content> <cert> <includeRoot> <pool...>, post_build <req> <out>, pem_subjects <antwort> (schreibt cert-N.pem nach $E2E_TMP). Der Abschnitt files prüft jetzt zwei Ketten statt leerer chains.
  • Rebuild dauerte rund 3 Minuten; im Hintergrund starten und mit until grep -qx done <log> warten (Monitor ist nicht verfügbar).

Task 3

Commit: 1554ae8 feat(cert-manager): Hersteller-ZIP, PKCS#7, eingefügter Text, Analysieren und Aufteilen (22 Dateien, 7 neu, keine Löschungen). Nicht gepusht.

Gates (gemessen, Stack neu gebaut mit docker compose up -d --build api web)

Gate Ergebnis
api vitest src/cert-manager src/module-registry 13 Dateien, 313 Tests grün (zip-expand 15, cert-model 21, cert-analyze 12)
web vitest modules/cert-manager src/messages 10 Dateien, 89 Tests grün (inkl. umlaut-guard)
tsc api / web ohne Fehler
biome lint beide Modulordner, biome check eigene Dateien sauber
check-cert-messages.cjs 40 messages ok 107
keine forge-Zertifikatparser grün
e2e-cert.sh all e2e cert files ok, e2e cert fullchain ok, e2e cert zip ok
gesamte <automated>-Kette letzte Zeile task3 ok

Live bewiesen (Abschnitt zip): Hersteller-ZIP (EC-Server, Zwischen, Wurzel, __MACOSX, Inner-ZIP, readme.txt) plus rsa-leaf und rsa-inter ergibt fünf Zertifikate, zwei Ketten, ignored nestedZip und unknown (readme.txt), MACOSX kommt in der Antwort nirgends vor; dasselbe ZIP als bundle.dat wird geöffnet; rsa-chain.p7c (DER) und ec-chain.p7b (PEM, EC) liefern je drei Zertifikate; Fullchain aus den aus dem ZIP erkannten EC-Zertifikaten hat zwei Blöcke und besteht openssl verify.

Abweichungen

  1. [Rule 3 - Blocking] MergeTab.test.tsx angefasst (nicht in der Dateiliste des Plans). CertWorkspace hat jetzt addText; die Workspace-Attrappe dieses Tests brauchte eine Zeile addText: () => null, sonst bricht tsc. Sonst unverändert.
  2. cleanSourcePath nach cert-names.ts verschoben (gleiche Importschleifen-Vermeidung wie safeBaseName in Task 2): zip-expand.ts braucht es, cert-analyze.ts importierte es bisher selbst. cert-analyze.ts exportiert es weiter (export { cleanSourcePath }), die Specs laufen unverändert.
  3. Typ-Umgehung bei forge: forge.asn1.fromDer(bytes, options) nimmt zur Laufzeit ein Optionsobjekt, die Typen kennen nur boolean. { decodeBitStrings: false } as unknown as boolean mit Erklärung im Code. Ohne decodeBitStrings: false stimmten die Zertifikatsbytes nach dem erneuten Schreiben nicht mehr mit dem Original überein (anderer Fingerabdruck); die Spec prüft die Kennungen gegen die Einzelzertifikate.
  4. Das Verschachtelte-ZIP-Erkennen geschieht zuerst am Namen (.zip, ohne Entpacken) und danach an den Anfangsbytes des entpackten Eintrags (ohne die Endung). Beides meldet nestedZip.
  5. Verschlüsseltes ZIP: encrypted-entry.zip meldet den Eintrag mit Pfad <zip>/rsa-leaf.pem und encryptedZip. Ein ZIP, aus dem gar nichts Lesbares und nichts Ausgelassenes entsteht (leeres Archiv), ergibt unknown für das ZIP selbst, damit die Oberfläche nie schweigt.
  6. Keine Stubs. detectPkcs12, detectPrivateKey, detectCsr sind weiter die benannten leeren Stufen für Task 4.

Bedrohungsstatus

  • Zip-Bombe / Speicher (T-ikt-02): alle Grenzen auf den Kopfdaten vor dem ersten getData() (Spec zählt Entpackungen: 0 bei zipTooLarge); Einzelgröße 1 MiB, Verhältnis 100, Summe 20 MiB, 100 Einträge, eine Ebene; Ergebnis des Entpackens wird nochmals gegen die Grenze geprüft.
  • Pfade (T-ikt-12/13): Eintragsnamen nur zur Anzeige, Steuerzeichen entfernt, höchstens 255 Zeichen, nie ein Dateipfad. Web zeigt sie als React-Text.
  • Kaputte Eingaben (T-ikt-03): ZIP-, PKCS#7- und Texterkennung stehen in try/catch (Zufallsbytes mit ZIP-Anfang: brokenZip, abgeschnittenes PKCS#7: unknown).
  • Keine Passwörter oder Schlüssel in diesem Task im Spiel; keine Logger-Aufrufe mit Inhalten. Eingefügter Text lebt nur im Browserarbeitsbereich (D-11).

Hinweise für Task 4

  • API cert-model.ts: Stufen detectPkcs12, detectPrivateKey, detectCsr sind weiter leere Slots (Reihenfolge in STAGES: ZIP, PEM, DER-Zertifikat, PKCS#12, PKCS#7, Schlüssel, CSR). Die PEM-Schleife in detectPem hat den Kommentar für PRIVATE KEY / ENCRYPTED / CERTIFICATE REQUEST. Rekursion: detectZip ruft detectBlob je ZIP-Eintrag mit ctx.path = "zip/eintrag" und gleichem file und passwords; Passwörter pro Datei und Container (auch im ZIP) funktionieren damit ohne weitere Änderung der ZIP-Stufe. Ein ZIP-Eintrag, der zu einer gesperrten PFX wird, liefert locked mit dem Pfad des Eintrags.
  • forge ist in cert-model.ts als import * as forge from 'node-forge' eingebunden (nur ASN.1 und util). Hilfsfunktion pkcs7Certificates(der) zeigt den ASN.1-Lauf; für den CSR-Lauf dieselbe Optionen-Umgehung (decodeBitStrings: false) verwenden, wenn Bytes unverändert bleiben sollen.
  • Web: CertWorkspace hat addText(text, label?); WorkingEntry.origin kennt paste, Dateien heißen pasted-<n>.pem (Nummer zählt weiter nach Entfernen). ItemCard nimmt nur CertItem; Task 4 ergänzt Karten für Schlüssel und CSR (Rollenfarben in ROLE_STYLES sind schon da) und die Zuordnung (certIds, csrIds, keyId). AnalyzeTab filtert aktuell kind === 'certificate'; Schlüssel/CSR/Gesperrte kommen dort dazu (files.ignored.unsupportedKey existiert). SplitTab ist bereits über alle Arten generisch (.crt, .key, .csr; Schlüssel-PEM aus item.pem).
  • Messages: tabs.analyze, tabs.split, analyze.*, split.*, files.paste*, files.pastedLabel, files.originPaste, files.rejected.pasteTooLarge neu; check-cert-messages.cjs zählt jetzt 107 Schlüssel (Mindestzahl 50 im Task-4-Gate ist damit schon erfüllt, ruhig höher ansetzen). Die Umlaut-Allowlist blieb unverändert.
  • e2e-cert.sh: BUILT="files fullchain zip"; analyze nimmt jetzt auch absolute Pfade (z. B. ZIP in $E2E_TMP) in der Form /pfad/datei:anzeigename. Der Abschnitt zip baut sein ZIP mit python3 -I nach $E2E_TMP/vendor.zip.
  • Rebuild dauerte wieder rund 3 Minuten (im Hintergrund starten, until grep -qx done <log>).

Task 4

Commit: fcac0a3 feat(cert-manager): Schlüssel, PFX und CSR erkennen, Passwort je Datei (28 Dateien, 7 neu, keine Löschungen). Nicht gepusht. Das e2e-Skript unter .planning ist wie vorgegeben nicht eingecheckt.

Gates (gemessen, Stack neu gebaut mit docker compose up -d --build api web)

Gate Ergebnis
api vitest src/cert-manager src/module-registry 16 Dateien, 389 Tests grün (cert-keys 32, cert-pkcs12 14, cert-csr 7, cert-analyze 21, cert-chain 18, controller 18)
web vitest modules/cert-manager src/messages 10 Dateien, 106 Tests grün (inkl. umlaut-guard)
tsc api / web ohne Fehler
biome lint beide Modulordner, biome check eigene Dateien sauber (10 eigene Dateien mit --write formatiert, nur eigene Dateien)
check-cert-messages.cjs 50 messages ok 135
keine forge-Zertifikat-/CSR-/PKCS#7-Leser im Produktivcode grün
e2e-cert.sh all e2e cert files ok, fullchain ok, zip ok, inputs ok
gesamte <automated>-Kette letzte Zeile task4 ok

Live bewiesen (Abschnitt inputs): Satz aus RSA-Server/Zwischen/Wurzel, klassisch verschlüsseltem RSA-Schlüssel, RSA-CSR, ec-compat.pfx und EC-CSR (DER) mit passwords im Takt der Dateien: sechs Zertifikate, zwei Schlüssel, zwei Anfragen, nichts gesperrt; RSA-Server trägt keyId, der EC-Server aus der PFX ebenfalls, beide CSRs sind Zertifikat und Schlüssel zugeordnet; rsa-modern.pfx ohne Passwort passwordNeeded (Container pkcs12), mit falsch passwordWrong; rsa-legacy.pfx (RC2) und rsa-modern.bin (ohne Endung) liefern je drei Zertifikate plus Schlüssel; verschlüsselter EC-Schlüssel (DER) gesperrt bzw. passwordWrong; passwords=nope -> 400 invalidInput; docker compose logs api --since 10m enthält weder Test-Pass-123 noch PRIVATE KEY.

Abweichungen

  1. TDD-Reihenfolge. Die Specs für Schlüssel, PFX und CSR wurden zusammen mit der Umsetzung geschrieben, nicht vorher rot gesehen; alle Fälle der <behavior> sind abgedeckt und liefen beim ersten Lauf grün.
  2. describeKey, keyIdOf, sha256Hex nach cert-keys.ts verschoben (Importschleife wie bei cert-names in Task 2/3: cert-keys/cert-csr/cert-pkcs12 brauchen sie, cert-model bindet sie ein). cert-model.ts exportiert sie weiter, alle bisherigen Importe laufen unverändert.
  3. CertItem.keyId und CsrItem.keyId sind nach dem Parser null (vorher Schlüsselkennung des Zertifikats). matchKeys setzt sie erst, wenn der passende Schlüssel in der Menge liegt; so bedeutet „keyId gesetzt“ wirklich „passender Schlüssel vorhanden“. Die eine Prüfung in cert-model.spec.ts wurde angepasst.
  4. readPkcs12(der, passwords, ownPassword = ''): zusätzlicher dritter Parameter, damit „Passwort falsch“ (eigenes Passwort eingegeben) von „Passwort nötig“ unterschieden wird. DetectContext hat dafür das optionale Feld ownPassword; passwords ist die Kandidatenliste (eigenes zuerst, höchstens 10, candidatePasswords in cert-keys.ts). PKCS#12 probiert zusätzlich das leere Passwort (nicht mitgezählt).
  5. Rule 3 - Blocking: MergeTab.test.tsx und SplitTab.test.tsx angefasst (nicht in der Dateiliste): CertWorkspace hat jetzt setPassword, die Attrappen brauchten eine Zeile setPassword: () => {}, sonst bricht tsc (gleiche Lage wie addText in Task 3).
  6. ItemCard nimmt jetzt item (Zertifikat, Schlüssel oder Anfrage) und items statt cert; innen gibt es CertCard, KeyCard, CsrCard. Einziger Aufrufer ist AnalyzeTab.
  7. forge.pki.RDNAttributesAsArray fehlt in den Typdefinitionen: mit einer schmalen Typumgehung an einer Stelle in cert-csr.ts verwendet (liest nur den Namen; ist kein Zertifikat-/CSR-Parser).
  8. Keine Stubs. cert-types.ts blieb unverändert (die Typen standen seit Task 1).

Bedrohungsstatus

  • T-ikt-08 (Schlüssel/Passwörter): Passwörter nur im Browserzustand und im multipart-Körper (passwords), nie in Adresse, Dateiname, Fehlertext, Antwort oder Log; Specs prüfen die Antwort (API) und den dargestellten Text (Web), das e2e prüft das API-Log. Fehlertexte aus OpenSSL/forge werden nicht weitergereicht.
  • T-ikt-03 (kaputte Eingaben): jede Schlüssel-, PKCS#12- und CSR-Stufe in try/catch; kaputter Schlüsselblock -> unsupportedKey, kaputtes DER -> unknown; Specs mit abgeschnittenem CSR, Zufallsbytes, kaputtem Base64.
  • Eingabegrenzen passwords: JSON-Liste aus höchstens 30 Zeichenketten zu höchstens 1024 Zeichen, sonst 400 invalidInput (Controller-Spec, Grenzfälle 30 x 1024 erlaubt, 31 und 1025 abgewiesen); je Datei höchstens 10 verschiedene Kandidaten (CPU-Schutz bei PBKDF).
  • Der Schlüssel (unverschlüsseltes PKCS#8) steht in der analyze-Antwort für die Oberfläche (Task 5 braucht ihn für die Ausgabe); er wird nicht geloggt (Request-Log kennt nur Pfad und Status).

Hinweise für Task 5

  • API: cert-keys.ts enthält keyItemFromObject, spkiDerOf, candidatePasswords, MAX_PASSWORDS; exportKey ist noch nicht da (laut Plan Task 5), ebenso writePkcs12 in cert-pkcs12.ts (dort readPkcs12, isPkcs12Der). KeyItem.pem ist immer unverschlüsseltes PKCS#8; createPrivateKey(item.pem) liefert das KeyObject für Export und PFX. cert-output.ts unverändert (Teilung/Fullchain von Task 2).
  • matchKeys(certs, keys, csrs) in cert-chain.ts mutiert die Einträge (setzt keyId, certIds, csrIds) und ist wiederholbar; analyzeWorkingSet(files, passwords) ruft es nach dem Zusammenfassen auf. csrPublicKeyOf(pem) (cert-csr.ts) liefert Kennung und SPKI-DER einer Anfrage.
  • Controller: parsePasswords(raw) (exportiert) liest das Feld passwords; analyze(files, rawPasswords?).
  • Web: WorkingEntry.password wird jetzt befüllt (setPassword(id, password) im Hook, setEntryPassword in working-set.ts); toFormData sendet passwords nur, wenn mindestens eins gesetzt ist. PasswordInput (Label, Wert, Anzeigen/Verbergen) ist wiederverwendbar, z. B. für das PFX-Passwort im Konvertieren-Reiter. ItemCard ist über item/items generisch; FilesTab zeigt gesperrte Dateien mit UnlockPanel (ruhiger Hinweis + „Trotzdem entsperren“, wenn schon ein Serverzertifikat mit keyId vorliegt und nur eine PFX ohne Passwort gesperrt ist).
  • Messages: neu files.locked.*, files.keyWasEncrypted, passwordInput.*, analyze.matchingKey/keysTitle/csrsTitle/lockedTitle/lockedNeeded/lockedWrong/lockedHint/wasEncrypted/belongsTo/noCertificateYet/subject/matchedKey/matchedCertificates/matchedCsr/present/absent/explainKey/explainCsr; check-cert-messages.cjs zählt jetzt 135 Schlüssel (Mindestzahl in Task 5 erhöhen). Umlaut-Allowlist: Passender, Passendes, Passende ergänzt.
  • e2e-cert.sh: BUILT="files fullchain zip inputs"; analyze_pw <passwörter-json> <out> <datei[:name]>... schickt das Feld passwords (nur für diesen Aufruf, lokale Variable ANALYZE_PASSWORDS). Abschnitt inputs prüft am Ende das API-Log der letzten 10 Minuten auf Passwort und Schlüssel.
  • Rebuild dauerte wieder rund 3 Minuten (im Hintergrund starten, until grep -qx done <log>).

Task 5

Commit: 65a1dca feat(cert-manager): alle Ausgabeformate, Konvertieren, Bundle und PFX (18 Dateien, 3 neu, keine Löschungen). Nicht gepusht. Das e2e-Skript unter .planning ist wie vorgegeben nicht eingecheckt.

Gates (gemessen, Stack neu gebaut mit docker compose up -d --build api web)

Gate Ergebnis
api vitest src/cert-manager src/module-registry 16 Dateien, 449 Tests grün (cert-output 45, cert-pkcs12 22, cert-keys 39, controller 28)
web vitest modules/cert-manager src/messages 11 Dateien, 121 Tests grün (inkl. umlaut-guard; ConvertTab 7, MergeTab 17)
tsc api / web ohne Fehler
biome lint beide Modulordner, biome check eigene Dateien (api cert-manager, web components, page, actions, working-set(+test), use-cert-workspace, Seitentest, de/en.json, umlaut-dictionary) sauber
check-cert-messages.cjs 60 messages ok 176
keine forge-Zertifikat-/CSR-/PKCS#7-Leser im Produktivcode grün
e2e-cert.sh all files, fullchain, zip, inputs, formats ok
gesamte <automated>-Kette letzte Zeile task5 ok

Output-Punkt 2 (openssl liest die Ausgaben, live gegen die API)

openssl pkcs12 -info -noout -passin pass:Neu-Pass-2026 (RSA; EC verhält sich gleich und ist im Abschnitt ebenfalls geprüft):

kompatibel: MAC: sha1, Iteration 2048
            Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048
modern:     MAC: sha1, Iteration 2048
            Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA1

openssl pkcs7 [-inform DER] -print_certs der Fullchain mit Wurzel, p7b | p7c, gleiche Reihenfolge Server, Zwischen, Wurzel:

rsa: www.example.test,Tessera Test Inter RSA,Tessera Test Root RSA | www.example.test,Tessera Test Inter RSA,Tessera Test Root RSA
ec:  ec.example.test,Tessera Test Inter EC,Tessera Test Root EC   | ec.example.test,Tessera Test Inter EC,Tessera Test Root EC

Außerdem live (RSA und EC): leaf DER (openssl x509 -inform DER), leaf p7b mit genau einem Zertifikat, Fullchain ohne Wurzel p7b mit zwei, Nur Kette p7c nur mit dem Zwischenzertifikat, Bundle (erstes Zertifikat der Server, zwei Zertifikate, openssl pkey -pubout gleich openssl x509 -pubkey), in beiden PFX-Profilen Schlüssel und Serverzertifikat passen zusammen, PKCS#8 mit Passwort (ENCRYPTED PRIVATE KEY, openssl pkey -passin ok), klassisch BEGIN RSA PRIVATE KEY / BEGIN EC PRIVATE KEY, DER mit Passwort, CSR als DER und PEM (openssl req -subject). Fehlercodes live: passwordRequired, keyMismatch, keyMissing, invalidInput (csr mit p7b). docker compose logs api --since 10m enthält weder Test-Pass-123 noch Neu-Pass-2026 noch PRIVATE KEY.

Abweichungen

  1. TDD-Reihenfolge. Specs für exportKey, writePkcs12, buildOutput, DTO und die Web-Reiter wurden vor der Umsetzung geschrieben, aber nicht einzeln rot gesehen (gleiche Lage wie Task 4); alle Fälle der <behavior> laufen grün.
  2. actions.ts unverändert. Die Typen und buildOutput mit dem vollen BuildInput standen seit Task 1 dort; es gab nichts anzupassen, die Datei ist nicht im Commit. Aus demselben Grund fehlt cert-json-body.spec.ts im Commit: die Größtanfrage (Task 2) deckt keyPem, csrPem und password schon ab und läuft unverändert grün.
  3. Neue Eingabegrenze/Fehlerabbildung in buildOutput: unbekannter Inhalt oder Format-Inhalt-Paar ergibt jetzt invalidInput (vorher formatNotPossible); formatNotPossible bleibt für „klassisch“ bei Schlüsseln, die weder RSA noch EC sind. Ein keyPem, den Node nicht öffnet (etwa verschlüsselt), ergibt invalidInput, ohne Schlüsseltext in der Meldung.
  4. Schlüssel- und Anfrage-Ausgabe ohne certPem: key und csr brauchen kein Zertifikat (Konvertieren-Reiter); der Dateiname kommt aus baseName, sonst schluessel bzw. der Name der Anfrage. Nur „Zertifikat“-Inhalte verlangen certPem.
  5. PFX-Anzeigename als BMPString: friendlyName geht UTF-16BE-kodiert an forge (forge schreibt den Wert sonst roh in ein BMPString-Feld und openssl zeigte Zeichensalat).
  6. Konvertieren sendet bei Schlüsseln baseName des zugehörigen Zertifikats (aus certIds), damit die Datei nicht „schluessel.key“ heißt; bei Zertifikaten kein baseName (API nimmt den eigenen).
  7. Rule 3 - Blocking: umlaut-dictionary.ts um passwortgeschützte und AES ergänzt (Guard).
  8. PFX ohne passenden Schlüssel bleibt erlaubt (nur Zertifikate, mit ruhigem Hinweis in der Oberfläche); die API verlangt den Schlüssel dafür nicht (laut Plan: „without key → certificates only“).
  9. Keine Stubs.

Bedrohungsstatus

  • T-ikt-08 (Schlüssel/Passwörter): Passwörter und Schlüssel nur im Anfragekörper; nirgends in Antwort (Spec: PFX- und Schlüssel-Antwort enthalten das Passwort nicht), Fehlertext, Dateiname oder Log (e2e prüft das API-Log). Passwortfelder der Oberfläche bleiben im Reiterzustand, Specs prüfen, dass sie nicht im dargestellten Text stehen. Fehlertexte aus node:crypto/forge werden nicht weitergereicht.
  • Anfragegrenzen: DTO-Whitelist (ValidationPipe) entfernt Fremdfelder; neue Felder mit MaxLength (keyPem, csrPem 16 384, password 256), IsIn für Inhalt, Format und pfxEncryption; die Größtanfrage bleibt unter der 512-KiB-Grenze von build (Spec aus Task 2 läuft unverändert).
  • Forge-Austausch beim PFX-Schreiben (D-20): ein einziger synchroner Aufruf, die drei Funktionen stehen im finally wieder an ihrem Platz; Spec prüft das nach Erfolg und nach einem Fehler mitten in toPkcs12Asn1 (der Durchreicher war währenddessen wirklich eingesetzt).
  • Zuordnung: PFX und Bundle prüfen den Schlüssel gegen das Serverzertifikat mit checkPrivateKey (Falsch: 400 keyMismatch), nie nach Namen.

Hinweise für Task 6

  • API: buildOutput in cert-output.ts ist die eine Funktion; Hilfsfunktionen darin sind parseCertificate, parseKey, matchingKey(head, keyPem), pkcs7Der, certListFile, joinPem, derOf, file(filename, data, mime). Die Kette (shown = Pfad ohne Wurzel, mit Wurzel bei includeRoot) wird dort einmal gebaut; Vorlagen können path/shown/head ebenso nutzen (einen eigenen case oder eine eigene Funktion buildTemplate in cert-templates.ts, die buildOutput für template aufruft). writePkcs12({ keyObject, certDers, password, profile, friendlyName }) und exportKey(key, 'pkcs8' | 'traditional' | 'pkcs8-der', password?) sind fertig; für NPM: exportKey(key, 'traditional') liefert PKCS#1 (RSA) bzw. SEC1 (EC) unverschlüsselt. Fehler: certError('templateNeedsKey', 400, ...) ist im Typ schon da.
  • DTO dto/cert-build.dto.ts: BUILD_CONTENTS, BUILD_FORMATS, BUILD_PFX_ENCRYPTIONS; template (IsIn der Vorlagenkennungen) und gegebenenfalls Inhalt template fehlen noch. Das BuildInput-Feld template?: string und BuildResult.snippet stehen schon in cert-types.ts und actions.ts. Die Formattabelle FORMATS in cert-output.ts legt je Inhalt die erlaubten Formate fest; ein neuer Inhalt template braucht dort einen Eintrag (oder die Vorlage läuft vor der Formatprüfung).
  • Web: PfxOptions (password, repeat, encryption, Callbacks) und pfxPasswordReady(password, repeat) sind für den IIS- und Tomcat-Vorgabewert wiederverwendbar (Kompatibel ist Vorgabe, PfxEncryption exportiert). PasswordInput bleibt das Passwortfeld. In page.tsx ist TabId jetzt files | analyze | split | merge | convert; templates kommt hinter convert (D-23).
  • Messages: neu tabs.convert, merge.formatLabel/formats.*/bundle*/pfx*/downloadBundle/downloadPfx, pfx.*, convert.*; check-cert-messages.cjs zählt jetzt 176 Schlüssel (Mindestzahl in Task 6 höher ansetzen). Umlaut-Allowlist: passwortgeschützte, AES ergänzt.
  • e2e-cert.sh: BUILT="files fullchain zip inputs formats"; neue Hilfen mkbody <out> '<json mit @fixture>' (Werte, die mit @ beginnen, werden durch den Inhalt der Fixture ersetzt), build_files <ordner> <json> (200 erwartet, legt alle Antwortdateien im Ordner ab) und build_fails <status> <code> <json>. Für Vorlagen mit ZIP-Antwort: das Archiv entsteht im Browser (fflate), die API liefert einzelne Dateien plus snippet.
  • Rebuild dauerte wieder rund 3 Minuten (im Hintergrund starten; bei TMPDIR-losen Shells den Scratchpad-Pfad für das Log verwenden).

Task 6

Commit: 47b2621 feat(cert-manager): Vorlagen für Zielsysteme, Modulversion 1.2.0 und Anleitungen (19 Dateien, 4 neu, keine Löschungen). Nicht gepusht. Das e2e-Skript unter .planning ist wie vorgegeben nicht eingecheckt.

Gates (gemessen, Stack neu gebaut mit docker compose up -d --build api web)

Gate Ergebnis
api vitest src/cert-manager src/module-registry 17 Dateien, 476 Tests grün (cert-templates 25, cert-output 47; module-changelog.spec mit 1.2.0 grün)
web vitest modules/cert-manager src/messages 12 Dateien, 133 Tests grün (TemplatesTab 11, Seitentest mit sechs Reitern, umlaut-guard)
tsc api / web ohne Fehler
biome lint beide Modulordner, biome check eigene Dateien sauber
check-cert-messages.cjs 80 messages ok 228
1.2.0 / 2026-10-09 im Changelog, CHANGELOG-Punkt, Anleitungs-Gates, keine forge-Zertifikat-/CSR-/PKCS#7-Leser grün
e2e-cert.sh all files, fullchain, zip, inputs, formats, templates, version ok
gesamte <automated>-Kette letzte Zeile task6 ok

Live bewiesen (Abschnitt templates, RSA und EC): Nginx und Apache openssl verify der Fullchain gegen die Wurzel (zwei Zertifikate ohne Wurzel, drei mit includeRoot), privkey.pem ist PKCS#8 und passt zum Serverzertifikat; Apache älter: cert.pem allein, chain.pem nur das Zwischenzertifikat, Verify besteht; IIS: PFX ohne pfxEncryption in der Anfrage, openssl pkcs12 -info zeigt pbeWithSHA1And3-KeyTripleDES-CBC, Schlüssel passt; Nginx Proxy Manager: BEGIN RSA PRIVATE KEY bzw. BEGIN EC PRIVATE KEY, certificate.pem und intermediate.pem getrennt, Schnipsel leer; HAProxy: erstes Zertifikat ist der Server, Schlüssel hinter den Zertifikaten; Tomcat: .p12 mit Passwort lesbar, Schnipsel mit IHR-PASSWORT und ohne das echte Passwort. Fehler mit Code: templateNeedsKey, keyMismatch, passwordRequired (iis und tomcat); unbekannte Vorlage 400 (durch die DTO-Prüfung, ohne Code). API-Log ohne Neu-Pass-2026 und PRIVATE KEY. Abschnitt version: Changelog erster Eintrag 1.2.0 vom 2026-10-09, danach 1.1.0; Katalog nennt cert-manager mit 1.2.0.

Abweichungen

  1. TDD-Reihenfolge. cert-templates.spec.ts wurde zusammen mit der Umsetzung geschrieben; die Fälle der <behavior> sind abgedeckt. Zwei Spec-Annahmen wurden im ersten Lauf korrigiert (der Anzeigename steckt verschlüsselt im Container und kommt von forge als roher BMPString zurück).
  2. Neue Dateien nicht nötig, aber cert-templates.ts kennt cert-output.ts nicht (Importschleife): file, joinPem, derOf stehen dort in Kurzform noch einmal. isTemplateId ist exportiert, buildOutput prüft die Kennung vor dem Schlüssel.
  3. BuildContent hat den neuen Inhalt template (cert-types.ts, DTO BUILD_CONTENTS, Web actions.ts mit TemplateId); FORMATS.template = ['template'], ein mitgesendetes format ergibt invalidInput. snippet steht nur in der Antwort von Vorlagen.
  4. Apache-legacy und Nginx Proxy Manager ohne Aussteller: chain.pem bzw. intermediate.pem entfallen (und die Zeile SSLCertificateChainFile), statt eine leere Datei zu liefern. Mit Kette sind es wie geplant drei Dateien.
  5. Nginx Proxy Manager, Schlüssel: bei Schlüsseltypen ohne klassisches Format (nicht RSA/EC) fällt die Vorlage auf PKCS#8 zurück statt mit formatNotPossible zu scheitern.
  6. Unbekannte Vorlagenkennung über HTTP: die DTO-Prüfung (IsIn) antwortet mit Nests Standard-400 ohne Code; invalidInput mit Code gibt es nur direkt aus buildOutput (Spec). Die Oberfläche kann es nicht auslösen.
  7. CHANGELOG: drei statt zwei Punkte unter „Neu“ (Arbeitsbereich, Zusammenführen/Formate/Konvertieren, Vorlagen) und ein Punkt unter „Behoben“; Modulversion 1.2.0 im ersten Punkt.
  8. Rule 3 - Blocking: umlaut-dictionary.ts um ssl, neuer, klassischen, SSLHostConfig, PASSWORT ergänzt (Guard).
  9. Betriebsanleitung: zusätzlich zur Zeile in „Fehlerbilder“ (413 beim Hochladen) ein Satz zum Rechenaufwand bei Passwörtern; Abschnitt sagt auch, dass der Zertifikat-Manager von sich aus nichts im Internet abruft (Task 7 ändert diesen Satz auf den Knopf „Fehlendes Zertifikat holen“).
  10. Keine Stubs.

Bedrohungsstatus

  • T-ikt-08 (Schlüssel/Passwörter): Vorlagen-Passwörter nur im Anfragekörper; Schnipsel enthält IHR-PASSWORT, nie das Passwort (Spec + e2e); Passwort steht nicht im dargestellten Text der Oberfläche (Test); API-Log im e2e ohne Passwort und Schlüssel; Fehlertexte ohne Schlüssel/Passwort (Spec).
  • Vorlagen verlangen immer ein passendes Schlüssel-Zertifikat-Paar (checkPrivateKey, sonst keyMismatch); die Reihenfolge der Kette baut die API selbst.
  • Der Schlüssel liegt in den Dateien der Vorlagen unverschlüsselt (Zielsysteme erwarten das); Oberfläche und Anleitung weisen darauf hin, die Dateien zu schützen (HAProxy-Schritt).

Hinweise für Task 7

  • API: cert-templates.ts/cert-output.ts brauchen keine Änderung. Neue Dateien laut Plan: common/public-url-guard.ts (+Spec), cert-aia.ts, dto/cert-fetch-issuer.dto.ts; Controller bekommt fetch-issuer (Platzhalterkommentar steht schon im Klassenkopf). ChainInfo.gap.aiaUrls ist schon gefüllt, CertItem.aiaIssuerUrls ebenso.
  • Web: page.tsx hat jetzt TabId = files | analyze | split | merge | convert | templates. ChainView.tsx zeigt die Lücke („Zwischenzertifikat fehlt“) und ist der Ort für den Knopf; working-set.ts braucht origin: 'fetched' mit Host (steht im Typ); useCertWorkspace hat noch keine Funktion zum Hinzufügen nachgeladener Einträge (neben addFiles/addText ergänzen). Der Knopf soll in Zusammenführen und Vorlagen (beide nutzen ChainView) und Analysieren erscheinen, wo ChainView vorkommt.
  • Messages: neu tabs.templates, templates.* (Karten nginx, apache, apacheLegacy, iis, npm, haproxy, tomcat mit title, delivers, steps.1..3); check-cert-messages.cjs zählt 228 Schlüssel (Mindestzahl in Task 7 mindestens 228). Umlaut-Allowlist-Ergänzungen siehe oben.
  • e2e-cert.sh: BUILT="files fullchain zip inputs formats templates version"; run_section hat noch den Platzhalter aia) e2e_fail; neuen Abschnitt section_aia anlegen und dort einhängen. Der Abschnitt version prüft jetzt Changelog-Top-Eintrag 1.2.0: Task 7 ergänzt dort ein Item (Anzahl >= 5 bleibt gültig).
  • Docs: Anwenderanleitung hat den Abschnitt „Fehlendes Zertifikat holen“ noch nicht (nur der Satz im Zusammenführen-Absatz über „Fügen Sie das fehlende Zertifikat im Reiter ‚Dateien‘ hinzu“ ist anzupassen); Betriebsanleitung „### Zertifikat-Manager“ sagt aktuell „ruft von sich aus nichts im Internet ab“ (um den Knopf und die Ports 80/443 ergänzen); Entwicklungsanleitung: neuer Absatz neben „Zertifikat-Manager, Arbeitsbereich und Ketten (quick-261009-ikt)“, der Satz über fetch-issuer dort steht schon.
  • Rebuild dauerte wieder rund 3 Minuten (im Hintergrund starten; Log im Scratchpad).

Task 7

Commit: a2fc2cb feat(cert-manager): Fehlendes Zertifikat holen, gehärteter Adressschutz (32 Dateien, 5 neu, keine Löschungen). Nicht gepusht. Das e2e-Skript unter .planning ist wie vorgegeben nicht eingecheckt.

Gates (gemessen, Stack neu gebaut mit docker compose up -d --build api web)

Gate Ergebnis
api vitest src/cert-manager src/module-registry src/common src/favorites src/nextcloud-status 36 Dateien, 889 Tests grün (public-url-guard 51, cert-aia 26, controller 31; favorites und nextcloud-status unverändert grün)
web vitest modules/cert-manager src/messages 13 Dateien, 150 Tests grün (ChainView 9, working-set +5, FilesTab +2, AnalyzeTab +1, umlaut-guard)
tsc api / web ohne Fehler
biome lint beide Modulordner + common, biome check der Kettenliste sauber (nur eigene Dateien mit --write formatiert)
check-cert-messages.cjs 85 messages ok 237
CHANGELOG „IPv6“ unter Unveröffentlicht, Anleitung-Gate, Changelog-Gate, keine forge-Zertifikat-/CSR-/PKCS#7-Leser grün
e2e-cert.sh all files, fullchain, zip, inputs, formats, templates, version, aia ok
gesamte <automated>-Kette letzte Zeile task7 ok

Output-Punkt 3: Live-AIA-Ergebnis (kein CERT_E2E_OFFLINE nötig, der api-Container kommt ins Internet)

  • Blatt: das Zertifikat von letsencrypt.org (EC), Lücke afterLeaf mit Adresse http://ye2.i.lencr.org/.
  • Geholt über POST fetch-issuer: Server ye2.i.lencr.org, Name YE2, Datei YE2.crt; openssl verify -partial_chain -trusted <geholt> <blatt> bestanden.
  • Nächste Stufe: Analyse von Blatt plus YE2 ergibt den Weg letsencrypt.org, YE2 mit Lücke afterCa, es fehlt „Root YE“, Adresse http://ye.i.lencr.org/ (dort erscheint der Knopf erneut).
  • Offline-Fälle live: aia-private-leaf.pem ergibt 422 aiaInternal, rsa-leaf-noaki.pem 422 aiaMissing, Text 400 notACertificate, eine mitgeschickte url ändert nichts, 16 385 Zeichen 400, ohne Anmeldung 401. API-Log der letzten 10 Minuten enthält kein BEGIN CERTIFICATE.
  • Der echte Abruf lief durch den eigenen undici-Agent mit createGuardedLookup (der Lookup bei Verbindungsaufbau funktioniert also real, mit all-Option und ohne).

Abweichungen

  1. TDD-Reihenfolge. Die Guard-Umsetzung stand vor dem Guard-Spec; cert-aia.ts vor cert-aia.spec.ts. Alle Fälle der <behavior> sind abgedeckt und liefen grün (ein Spec-Fehler im ersten Lauf war ein Testartefakt: ein ReadableStream ruft pull einmal vorab auf, deshalb highWaterMark: 0).
  2. cert-model.ts angefasst (nicht in der Dateiliste): pkcs7Certificates ist jetzt exportiert (der D-16-Walker, von cert-aia.ts benutzt; kein zweiter Leser).
  3. Fünf Test-Attrappen angefasst (Rule 3, wie in Task 3/4): CertWorkspace hat jetzt addFetched; in AnalyzeTab.test.tsx, MergeTab.test.tsx, ConvertTab.test.tsx, SplitTab.test.tsx und TemplatesTab.test.tsx kam je eine Zeile addFetched: () => 'added' dazu. TemplatesTab.tsx reicht workspace.addFetched an ChainView (nicht in der Dateiliste, laut Task 6 aber ein ChainView-Nutzer, der Knopf soll auch in Vorlagen erscheinen).
  4. Guard strenger als der Plan: der ganze Bereich ::/96 ist immer gesperrt (der Plan wollte ihn nach der eingebetteten IPv4 beurteilen); dazu ::ffff:0:0:0/96 (IPv4-übersetzt, nach eingebetteter IPv4) und 3fff::/20 (Dokumentation). isPrivateIpv6 ist jetzt ebenfalls exportiert. Der Plan nannte nur isPrivateIpAddress.
  5. Port-Prüfung im Spec: das Fixture aia-private-leaf.pem hat keine Adresse mit Port 8080 (CA-Schlüssel sind gelöscht, keine Neuerzeugung). Die Port-Regel (hasDefaultPort, gleiche Bedingung wie der Adressschutz) wird deshalb über eine Weiterleitung auf :8080 im Spec bewiesen; für die erste Adresse nutzt der Code dieselbe Bedingung.
  6. Mehrere Adressen: bei Misserfolg gewinnt die Meldung mit dem meisten Fortschritt (aiaNotIssuer vor aiaTooLarge vor aiaUnreachable vor aiaInternal); eine Warnzeile je Aufruf mit allen versuchten Servernamen. Der Zeitrahmen von 8 s gilt je Adresse.
  7. Antwort host ist der Server der tatsächlich antwortenden Stelle (nach Weiterleitungen), nicht der erste.
  8. Web-Zusatzmeldungen: chain.fetchButton/fetching/fetchHint/fetchNoAddress/fetchAlready/fetchListFull/fetchListTooLarge/fetchFailed, files.fetchedFrom. WorkingEntry hat das optionale Feld pem (nur bei nachgeladenen Einträgen), damit dasselbe Zertifikat nicht doppelt angehängt wird. Bei einer Lücke ohne Adresse zeigt ChainView nur den Hinweis zum Herunterladen (hinter einem Zwischenzertifikat bleibt der ruhige Hinweis ohne Zusatz).
  9. CHANGELOG: der Sicherheitspunkt steht unter „Behoben“ (nicht „Neu“); das Zusammenführen-Bullet nennt den Knopf. Modul-Changelog 1.2.0 hat jetzt sechs Punkte.
  10. Keine Stubs.

Bedrohungsstatus

  • SSRF: Adresse nur aus dem Zertifikat (DTO-Whitelist, Spec und e2e mit mitgeschickter url); Standardport, kein Login in der Adresse, ≤ 2048 Zeichen, höchstens 3 Adressen; Adressschutz vor der ersten Anfrage und vor jedem Sprung (fetchImpl wird bei internen Adressen nie gerufen, Spec); DNS-Rebinding: createGuardedLookup im undici-Agent (Spec mit nachgebildeter Auflösung, live bewiesen); 8 s, 256 KiB (vorab und beim Lesen), 3 Weiterleitungen.
  • Fremdes Zertifikat: nur Aussteller mit checkIssued und Signaturprüfung; Spec mit EC-Zertifikat, gleichnamigem Zertifikat mit anderem Schlüssel (rsa-inter-decoy) und Nicht-Zertifikat-Antwort.
  • Log: genau eine Warnzeile mit Servername und Code, nie PEM oder Pfad (Spec, e2e).
  • Gemeinsame Nutzer des Guards (Favoriten-Symbole, Nextcloud-Logo): Specs unverändert grün; Wirkung nur strenger.
  • Akzeptierter Rest (im Kopfkommentar von cert-aia.ts): jeder angemeldete Benutzer des Moduls kann einen einzigen GET an eine öffentliche Adresse aus seinem hochgeladenen Zertifikat auslösen.

Hinweise für Task 8

  • Der Knopf erscheint in Analysieren, Zusammenführen und Vorlagen (alle drei nutzen ChainView mit onFetched={workspace.addFetched}). Für den Browsernachweis: letsencrypt.org-Blatt aus dem e2e (openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org) im Reiter „Dateien“ hochladen (wird hinter $E2E_TMP nicht aufbewahrt, neu holen), dann Reiter „Zusammenführen“: Meldung „Zwischenzertifikat fehlt“ mit Knopf und Hinweis auf ye2.i.lencr.org; Klick holt YE2, Eintrag „YE2.crt“ mit „nachgeladen von ye2.i.lencr.org“, darunter erscheint der Knopf erneut (Lücke hinter YE2, „Root YE“). Screenshots dunkel zuerst.
  • Gate-Mindestzahl check-cert-messages.cjs zählt jetzt 237 Schlüssel; e2e-cert.sh: BUILT="files fullchain zip inputs formats templates version aia", Abschnitt aia ist der letzte in all; CERT_E2E_OFFLINE=1 überspringt nur den Live-Teil.
  • Nachweisliste für Output-Punkt 4 (Task 8): alte Routen parse, split, merge, convert, export liefern 404 (das Setup des e2e prüft parse bei jedem Lauf), keine forge-Zertifikatleser im Produktivcode (Gate grün in jedem Task).
  • Checkliste für die echte Umgebung (Output-Punkt 6) um diesen Punkt ergänzen: Aus alpha muss der api-Container ausgehend per HTTP/HTTPS (Ports 80 und 443) ins Internet kommen, sonst meldet der Knopf „nicht erreichbar“ (Betriebsanleitung beschreibt Prüfbefehl und Fehlerbild).
  • Rebuild dauerte wieder rund 3 Minuten (im Hintergrund; Log im Scratchpad).

Task 8

Commit: keiner. Der Browser-Nachweis ergab keinen Befund, der eine Codeänderung nötig machte; die Anwenderanleitung stimmt mit den Beschriftungen der Seite überein. Die einzige Dateiänderung ist das Verschieben des Todos nach .planning/todos/completed/ (laut Vorgabe nicht eingecheckt, nur umbenannt, nicht gestaged). Nichts gepusht, nichts ausgerollt.

Gates (gemessen, Stack neu gebaut mit docker compose up -d --build api web, Seed-Zeile „Cert-Manager module seeded in registry“ im api-Log)

Gate Ergebnis
api vitest (gesamte Suite) 165 Dateien, 3497 Tests grün
web vitest (gesamte Suite) 158 Dateien, 1915 Tests grün (siehe Hinweis zu domains-page unten)
tsc api / web ohne Fehler
biome lint (api cert-manager + common, web Modulordner) und biome check der Dateiliste der Verify-Kette sauber
check-cert-messages.cjs 85 messages ok 237
Todo unter completed, nicht mehr unter pending ja
e2e-cert.sh all files, fullchain, zip, inputs, formats, templates, version, aia ok (inkl. Live-Abruf bei ye2.i.lencr.org)
8 dunkle und 4 helle Bilder 9 dunkle, 4 helle vorhanden
gesamte <automated>-Kette letzte Zeile task8 ok

Hinweis: Im ersten Lauf der Kette schlug einmal domains-page.test.tsx fehl (Zeile 312, waitFor mit mockSaveSettings, Zeitüberschreitung unter Last, während parallel Browser und Docker liefen). Die Datei gehört zu einem anderen Modul, wurde von diesem Auftrag nicht angefasst, lief danach dreimal einzeln grün (27 Tests) und im zweiten Gesamtlauf grün. Als zeitabhängig (flaky) eingestuft, nicht behoben (außerhalb des Auftrags).

Output-Punkt 4: alte Routen und forge

  • POST parse, split, merge, convert, export unter /modules/cert-manager/ liefern 404; das e2e (Abschnitt files) prüft das bei jedem Lauf und war grün. Der Controller kennt nur analyze, build und fetch-issuer.
  • Im Produktivcode von apps/api/src gibt es keine forge-Leser für Zertifikat, CSR oder PKCS#7 (grep nach certificateFromPem/Asn1, certificationRequestFromPem/Asn1, pkcs7.messageFromPem/Asn1: keine Treffer). forge bleibt nur für ASN.1, PKCS#12 und den Schreibweg.

Output-Punkt 5: Browser-Nachweis

Werkzeug: playwright-core mit dem lokalen Chromium (Playwright MCP stand nicht zur Verfügung), App unter http://localhost:3000, Anmeldung admin, Skript im Scratchpad (proof.cjs), nichts per fetch aus der Seite gemessen. Dunkelmodus zuerst über den Theme-Knopf, danach Hellmodus. Eingabedateien aus den Fixtures: server.crt (rsa-leaf), intermediate.crt (rsa-inter), zertifikat-paket.zip (EC-Server als ServerCertificate.crt, EC-Zwischen, EC-Wurzel, server.key, ein __MACOSX-Eintrag), rsa-compat.pfx, letsencrypt-leaf.pem (live von letsencrypt.org geholt).

Bilder unter /home/vicolab/projects/tessera-ctl/.playwright-mcp/cert-manager/ (Ordner ist gitignoriert):

Datei Zeigt
ikt-dark-empty.png leerer Arbeitsbereich, Hinweis zum Browserfenster
ikt-dark-files.png zwei getrennte Auswahlen (server.crt, dann intermediate.crt) beide in der Liste, dazu ZIP (je Pfad erkannt, __MACOSX fehlt) und entsperrte PFX (Zertifikate plus Schlüssel „war verschlüsselt“)
ikt-dark-analyze.png Ketten (EC und RSA), Karten je Zertifikat mit „Passender Schlüssel vorhanden“, Schlüsselkarten
ikt-dark-merge.png Zusammenführen für das EC-Serverzertifikat, Root-Haken aus, PFX-Optionen ausgefüllt (Kompatibel vorgewählt)
ikt-dark-convert.png Schlüssel, Zielformat Traditionell, Passwortschutz
ikt-dark-templates.png alle sieben Vorlagen, Windows-/IIS-Karte mit Passwort, Schnipsel sichtbar
ikt-dark-gap.png nur das Letsencrypt-Blatt: „Zwischenzertifikat fehlt: „YE2““ mit Knopf „Fehlendes Zertifikat holen“ und Hinweis auf ye2.i.lencr.org
ikt-dark-fetched.png nach dem Klick: Kette Server, YE2; darüber ruhiger Hinweis auf „Root YE“ mit erneutem Knopf (ye.i.lencr.org)
ikt-dark-mobile.png Reiter Dateien bei 390 x 844, Eintrag „YE2.crt“ mit „nachgeladen von ye2.i.lencr.org“
ikt-light-files.png, ikt-light-merge.png, ikt-light-templates.png, ikt-light-gap.png dieselben Ansichten im Hellmodus
step-after-reload.png, step-files-after-fetch.png, step-mobile-merge.png Hilfsbilder (leer nach Neuladen, Liste nach dem Nachladen, Zusammenführen mobil)

Gemessene Ergebnisse des Ablaufs:

  • Zwei getrennte Auswahlen bleiben beide in der Liste (der gemeldete Fehler ist behoben); ZIP und PFX kommen dazu, die PFX lässt sich mit dem Passwort entsperren (Schlüssel „RSA, 2048 Bit, war verschlüsselt“, Zertifikate erscheinen).
  • Root-Haken ist standardmäßig aus. Die im Browser heruntergeladene Fullchain ec.example.test-fullchain.pem enthält mit openssl geprüft genau zwei Zertifikate (Server, Zwischen), ohne Wurzel.
  • Die im Browser erzeugte PFX (Kompatibel) wurde mit openssl pkcs12 -info gelesen (MAC sha1, Zertifikat-Beutel); die Windows-/IIS-Vorlage liefert pbeWithSHA1And3-KeyTripleDES-CBC (Shrouded Keybag); die Nginx-Vorlage liefert ein ZIP aus fullchain.pem, privkey.pem und ANLEITUNG.txt; der Schlüssel in „Traditionell mit Passwort“ ist BEGIN EC PRIVATE KEY mit Proc-Type: 4,ENCRYPTED (AES-256-CBC) und lässt sich mit openssl ec -passin öffnen.
  • „Fehlendes Zertifikat holen“ holte live YE2 (Server ye2.i.lencr.org); die Dateiliste zeigt „YE2.crt, nachgeladen von ye2.i.lencr.org, Zwischenzertifikat YE2“; danach erscheint der Knopf eine Ebene höher (Root YE).
  • Nach dem Neuladen der Seite ist die Liste leer (kein Listenabschnitt) und der Hinweis „Die Dateien bleiben nur in diesem Browserfenster …“ sichtbar.

Design-Review gegen D-23 (jedes Bild einzeln angesehen):

  • Ruhige, dichte Liste, lesbare Rollenplaketten und Warnkasten in beiden Modi (gelb auf dunklem Braun bzw. dunkles Braun auf hellem Gelb), Passwortfelder und Auswahlfelder sind in beiden Modi als Felder erkennbar, kein Großschrift-Etikett, keine Pfeilzeichen oder Mittelpunkte in der Oberfläche, formale Anrede.
  • Fokus ist sichtbar (gelber Rand am Passwortfeld in ikt-dark-convert.png, hervorgehobener Hauptknopf in ikt-dark-merge.png).
  • Befund: keiner, der eine Änderung verlangt. Beobachtungen ohne Handlungsbedarf: Auf 390 Pixel Breite läuft die Reiterleiste seitlich weiter („Zusammenführen“ angeschnitten; gemeinsames Reiterbauteil, wischbar); die Zeilen mit PowerShell-Zeile in den Vorlagen laufen seitlich (Schnipsel scrollt in seinem Kasten); die Seitenleiste zeigt „Cert Manager“ als Modulname (Katalog, nicht Teil dieses Auftrags).
  • Zwischenstand der Bilder: Die ersten Aufnahmen zeigten bei langen Seiten die mitscrollende Kopfleiste mitten im Bild; das war ein Artefakt der Ganzseitenaufnahme (Scrollposition), nicht der Seite. Das Skript scrollt jetzt vor jeder Aufnahme nach oben, alle abgegebenen Bilder sind danach neu entstanden.
  • Anwenderanleitung: alle in „…“ und fett genannten Beschriftungen der Zertifikat-Manager-Anleitung kommen im Wortlaut auf der Seite vor (Dateien, Analysieren, Aufteilen, Zusammenführen, Konvertieren, Vorlagen, „Root-Zertifikat mitnehmen“, „Kompatibel (auch ältere Windows-Server)“, „Modern (AES-256)“, „Fehlendes Zertifikat holen“, „Zwischenzertifikat fehlt“, „nachgeladen von …“). Keine Korrektur nötig.

Abweichungen

  1. Keine Codeänderung und damit kein Task-8-Commit (der Plan sah ihn nur für die Todo-Verschiebung und Review-Befunde vor; die Anweisung dieses Laufs verbietet Einchecken unter .planning/). Das Todo wurde mit mv nach .planning/todos/completed/ verschoben, nicht mit git mv und nicht gestaged.
  2. Playwright MCP war nicht verfügbar; Ersatz: playwright-core mit dem lokalen Chromium (so vorgegeben).
  3. Ein einmaliger zeitabhängiger Fehlschlag von domains-page.test.tsx (siehe oben), bei Wiederholung grün.

Gesamtübersicht

Task Commit Inhalt
1 75ea83f Reiter „Dateien“, gemeinsamer Arbeitsbereich, ein Parser für RSA und EC (Durchstich)
2 30118a2 Zusammenführen, Kettenbildung, Fullchain und Nur Kette, eigene Körpergrenze für build
3 1554ae8 Hersteller-ZIP, PKCS#7, eingefügter Text, Analysieren, Aufteilen
4 fcac0a3 Schlüssel, PFX, CSR, Passwort je Datei
5 65a1dca alle Ausgabeformate, Konvertieren, Bundle, PFX (kompatibel und modern)
6 47b2621 Vorlagen, Modulversion 1.2.0, Changelogs, Anleitungen
7 a2fc2cb „Fehlendes Zertifikat holen“, gehärteter gemeinsamer Adressschutz
8 keiner Endgates, Browser-Nachweis (13 Bilder), Todo abgeschlossen

Alle acht Commits liegen lokal auf main, nichts gepusht, nichts ausgerollt. Der Stack läuft lokal mit dem Stand von Task 7.

Checkliste für die echte Umgebung (macht der Nutzer nach dem eigenen Pull auf alpha)

  1. Neubau mit --build bzw. --force-recreate (ein schlichtes up baut nicht neu); API-Log prüfen: „Cert-Manager module seeded in registry“, Modulversion 1.2.0 im Marktplatz.
  2. Echtes Hersteller-ZIP im Reiter „Dateien“ hochladen, Zusammenführen, Fullchain herunterladen und auf dem Zielsystem prüfen (ohne Wurzel, die meisten Server brauchen sie nicht).
  3. Nginx Proxy Manager: Vorlage „Nginx Proxy Manager“ benutzen und in „SSL Certificates“, „Add SSL Certificate“, „Custom“ die Dateien certificate.pem, intermediate.pem und privkey.pem in die Felder legen. Zu bestätigen (Annahme aus der Recherche A2): NPM akzeptiert die Dateien, und der Kopf BEGIN RSA PRIVATE KEY bzw. BEGIN EC PRIVATE KEY wird als Schlüssel angenommen. Dazu der Nginx-Proxy-Manager-Upload: große ZIPs/Dateien blockt NPM bei Uploads; die Dateien hier sind klein (wenige KB), die 413-Grenze der Plattform liegt bei 20 MiB gesamt und muss in NPM nicht angehoben werden, solange nur kleine Dateien hochgeladen werden. Bei 413 beim Hochladen einer größeren ZIP: Upload-Grenze des NPM-Hosts (client_max_body_size) prüfen.
  4. Windows-Server: die PFX „Kompatibel (auch ältere Windows-Server)“ (3DES/SHA-1, Vorgabe in der Vorlage „Windows / IIS“) importieren und im IIS-Manager an eine Bindung hängen; zusätzlich, wenn gewünscht, die PFX „Modern (AES-256)“ auf einem aktuellen Windows testen, um zu sehen, welche Systeme sie annehmen (Recherche A1). Beide Varianten sind mit OpenSSL lesbar; ob ein bestimmter Windows-Server sie importiert, ließ sich hier nicht prüfen.
  5. „Fehlendes Zertifikat holen“: aus dem api-Container auf alpha muss ausgehend per HTTP und HTTPS (Ports 80 und 443) ins Internet möglich sein, sonst meldet der Knopf „nicht erreichbar“ (Prüfbefehl und Fehlerbild stehen in der Betriebsanleitung). Test: Letsencrypt-Blatt hochladen, Reiter „Zusammenführen“, Knopf klicken, es muss YE2 nachgeladen werden. Interne CAs mit privaten Adressen werden absichtlich nicht angefragt (Meldung „aiaInternal“); dort das Zwischenzertifikat von Hand hinzufügen.
  6. Der gehärtete gemeinsame Adressschutz (Favoriten-Symbole, Nextcloud-Logo) ist strenger geworden: kurz prüfen, dass Favoriten-Symbole und das Nextcloud-Logo auf alpha weiterhin laden.
  7. Basic-Auth vor alpha bleibt wie sie ist; Updater-Verhalten ist von diesem Auftrag nicht berührt.

Review fixes

Alle Befunde aus 261009-ikt-REVIEW.md (CR-01 bis CR-03, WR-01 bis WR-07, IN-01 bis IN-05) sind behoben; Einzelheiten je Befund stehen im Abschnitt „Fix status“ der Review-Datei. Zwei lokale Commits, nichts gepusht, nichts ausgerollt:

  • bfcf6d4 fix(cert-manager): ZIP-Bombe, PEM-Scanner und Umlaut-Passwörter in PFX behoben (40 Dateien: API, 10 neue Fixtures, keine Löschungen)
  • c5bffe9 fix(cert-manager): Reiter zeigen nur noch passende Auswertungen, Aufteilen schützt Schlüssel (23 Dateien: Web, Meldungen, Anleitungen, CHANGELOG, keine Löschungen)

Der Trailer beider Commits ist Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> (laut Attributionsvorgabe der Umgebung; der Auftrag nannte „Opus 5.5“).

Abweichungen und Entscheidungen

  1. CR-03 ohne node:crypto-Schlüsselbeutel. Statt den PKCS#8-Beutel selbst zu bauen und zu öffnen, ersetzt withForgeKdf forges PBKDF2 für die Dauer des synchronen Aufrufs durch eine Ableitung aus UTF-8-Bytes mit node:crypto (gleiche Technik wie beim PFX-Schreiben, im finally zurückgesetzt, Spec prüft das). Damit stimmen Lesen und Schreiben mit OpenSSL 3 überein; alle Kombinationen (pässwörd, pw€, compat und modern) gehen, kein Passwort wird abgelehnt. Nebenwirkung: SHA-256/512-PBKDF2 läuft jetzt nativ statt in reinem JavaScript (7,5 µs je Runde vorher).
  2. Neue Fehler- und Ignorier-Codes: tooManyItems (413), aiaNotAllowed (422), protectionTooExpensive (Datei übersprungen). Dazu Meldungen (de/en), errors.buildFailed und workspace.stale. check-cert-messages.cjs zählt jetzt 250 Schlüssel (Mindestzahl künftig mindestens 250).
  3. WR-02 als fresh-Feld. CertWorkspace.fresh ist neu; Tests, die einen CertWorkspace nachbauen, brauchen das Feld (fünf Attrappen angepasst; tsc prüft Testdateien nicht, sie liefen sonst mit einer unsichtbaren Analyse).
  4. WR-03: Beim Aufteilen sind Downloads jetzt asynchron (Schlüssel mit Schutz kommen von build); die Tests von SplitTab warten mit waitFor. Die entsperrte Schlüsselanzeige in der Analyse-Antwort bleibt unverschlüsselt (dafür gebaut, nicht geloggt).
  5. main.ts verschlankt: die HTTP-Einrichtung liegt in http-setup.ts (configureHttp), damit die CORS-Reihenfolge testbar ist. Das alte Task-2-Gate „certBuildJsonBody in main.ts“ trifft damit nicht mehr zu (jetzt http-setup.ts).
  6. Fixtures: make-fixtures.sh um Umlaut-/Euro-PFX, Zu-teuer-Dateien und die drei Wurzeln ohne basicConstraints ergänzt; die neuen Dateien wurden mit denselben Befehlen gegen die vorhandenen Test-Zertifikate erzeugt (die Skriptdatei erzeugt bei einem Neulauf alles neu, wie gehabt). Die Specs rufen OpenSSL weiter nie auf.
  7. Modulversion bleibt 1.2.0: Punkte der bestehenden Einträge erweitert (Umlaut-Passwörter, geschützte Schlüssel beim Aufteilen), kein neuer Eintrag. CHANGELOG.md: Zusätze in den bestehenden Zertifikat-Manager-Punkten und im Sicherheitspunkt unter „Behoben“.
  8. e2e-cert.sh: neuer Abschnitt review (CR-01, CR-02, CR-03 in beide Richtungen mit openssl, WR-01, WR-04 bis WR-07), steht in BUILT; Datei liegt wie die anderen unter .planning und ist nicht eingecheckt.

Gates (gemessen, Stack neu gebaut mit docker compose up -d --build api web)

Gate Ergebnis
api vitest (gesamt) 169 Dateien, 3573 Tests grün (cert-manager neu u. a. pem-scan 10, cert-budget-Spezifisches in cert-pkcs12/cert-keys/cert-analyze, cert-upload 6, http-setup 3, cert-aia-url 13); enthält favorites, nextcloud-status, common
web vitest (gesamt) 160 Dateien, 1945 Tests grün (neu: use-cert-workspace 6, stale-analysis 15, SplitTab +9)
tsc api / web ohne Fehler
biome check auf allen geänderten und neuen Dateien (63 Pfade der Commits, davon 47 Quelldateien) ohne Befund
check-cert-messages.cjs 237 messages ok 250
e2e-cert.sh all files, fullchain, zip, inputs, formats, templates, version, aia (live, ye2.i.lencr.org), review: alle ok
Browser (dunkel) Datei entfernen: Zusammenführen und Aufteilen zeigen nur „Dateien werden geprüft …“ ohne Downloads, danach die neue Liste; fehlgeschlagene Prüfung: Meldung mit „Erneut versuchen“, keine Downloads; geschützter Schlüssel: Schutz vorgewählt, Datei ENCRYPTED PRIVATE KEY, mit neuem Passwort lesbar. Bilder in .playwright-mcp/cert-manager/ikt-fix-*.png (merge-before, merge-analysing, split-analysing, split-after-remove, merge-after-remove, split-error, split-key-protected, split-key-plain-warning)