fix(cert-manager): Reiter zeigen nur noch passende Auswertungen, Aufteilen schützt Schlüssel

- Ausgabe-Reiter bieten nur an, was zur aktuellen Dateiliste passt: nach dem Entfernen
  einer Datei oder einer fehlgeschlagenen Prüfung kommt Hinweis oder Fehler mit
  „Erneut versuchen“ statt der alten Auswertung (WR-02)
- Aufteilen: ein in der Datei geschützter Schlüssel bleibt geschützt (Passwort für die
  Ausgabe, auch in der ZIP); ohne Schutz nur ausdrücklich und mit sichtbarem Hinweis;
  Schlüssel heißen nach ihrem Zertifikat (WR-03, IN-05)
- Meldungen: tooManyItems, aiaNotAllowed, buildFailed (statt „Dateien konnten nicht
  geprüft werden“ beim Erstellen), protectionTooExpensive; Hinweis zum Passwort
  genauer (IN-03, IN-05)
- Download-Adresse wird erst nach einer Sekunde widerrufen (IN-04)
- Anleitungen und Änderungsliste: Grenzen, Aufteilen mit Schlüsselschutz, Umlaut-Passwörter

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-09 17:10:45 +02:00
parent ff2226dfbd
commit 4b87249a4a
23 changed files with 758 additions and 113 deletions
+34 -7
View File
@@ -909,22 +909,49 @@ deshalb für die Dauer **eines** synchronen Aufrufs drei Funktionen von `forge.p
(`privateKeyToAsn1`, `wrapRsaPrivateKey`, `certificateToAsn1`) gegen Durchreicher aus und stellt
sie im `finally` wieder her; ASN.1 für Zertifikate und Schlüssel kommt aus `node:crypto`, die
Verschlüsselung und den MAC macht weiterhin forge. Das ist sicher, weil Node einfädig ist und der
Aufruf nicht abgibt; das Spec prüft die Wiederherstellung auch nach einem Fehler. Profile:
Aufruf nicht abgibt; das Spec prüft die Wiederherstellung auch nach einem Fehler. **Passwörter:**
forge leitet den AES-Schlüssel (PBES2/PBKDF2) aus einem Byte je UTF-16-Einheit ab, OpenSSL 3, Windows
und Java nehmen die UTF-8-Bytes; mit Umlaut oder `€` wäre ein „modernes“ PFX sonst für jedes andere
Programm unlesbar (und umgekehrt, Review CR-03). Darum ersetzt `withForgeKdf` (Lesen **und**
Schreiben) zusätzlich `forge.pkcs5.pbkdf2` durch eine Ableitung mit `node:crypto` aus den UTF-8-Bytes
(auch SHA-256/512 nativ statt in reinem JavaScript). Die PKCS#12-eigene Ableitung (3DES, RC2, MAC)
bleibt, wie sie ist: sie nimmt BMPString (UTF-16) aus den Zeichen, genau wie OpenSSL. Dieselbe
Stelle zählt die Ableitungsrunden (siehe „Grenzen je Anfrage“). Profile:
`compat` (3DES, SHA-1, Vorgabe, lesbar bis Windows Server 2016) und `modern` (AES-256, der MAC bleibt
bei forge SHA-1). Die Vorlagen für IIS und Tomcat nutzen dieselbe Funktion (`cert-templates.ts`);
die Vorlagen für Dateien bauen aus denselben Bausteinen wie `buildOutput` und kennen `cert-output.ts`
bewusst nicht (sonst entstünde eine Importschleife).
*ZIP-Grenzen.* `zip-expand.ts` prüft **alles vor dem ersten Entpacken** an den Kopfdaten: höchstens
100 Einträge, 1 MiB je Eintrag, Verhältnis entpackt zu gepackt höchstens 100, Summe 20 MiB, nur eine
Ebene (ein ZIP im ZIP wird gemeldet, nicht geöffnet), verschlüsselte Einträge werden gemeldet.
Eintragsnamen dienen nur der Anzeige und werden nie als Dateipfad benutzt.
*ZIP-Grenzen.* `zip-expand.ts`: höchstens 100 Einträge, 1 MiB je Eintrag, Verhältnis entpackt zu
gepackt höchstens 100, Summe 20 MiB, nur eine Ebene (ein ZIP im ZIP wird gemeldet, nicht geöffnet),
verschlüsselte Einträge werden gemeldet. Die Kopfdaten (deklarierte Größe im Zentralverzeichnis)
sind nur Angreiferwunsch: adm-zip begrenzt die Ausgabe bei deklarierter Größe 0 gar nicht, ein
300-kB-ZIP entpackte dort zu 300 MiB (Review CR-01). Darum entpackt das Modul selbst
(`inflateRawSync` mit `maxOutputLength` = Einzelgrenze, höchstens die Restgrenze der Summe) und prüft
Größe, Verhältnis und CRC am echten Ergebnis; weicht die echte Größe von der deklarierten ab, gilt der
Eintrag als `suspicious`. Eintragsnamen dienen nur der Anzeige und werden nie als Dateipfad benutzt.
*Grenzen je Anfrage.* `cert-budget.ts` (`RequestBudget`, in `analyzeWorkingSet` einmal je Anfrage
angelegt und über `DetectContext.budget` durch alle Stufen gereicht): höchstens 200 Zertifikate, 50
Schlüssel, 50 Zertifikatsanfragen (413 `tooManyItems`; der Kettenbau ist quadratisch). Der Aufwand des
Passwortschutzes ist begrenzt (Review WR-04): eine Ableitung höchstens 1 000 000 Runden, alle zusammen
höchstens 6 000 000 je Anfrage; Runden werden vor dem Rechnen gemeldet (PKCS#12 über die drei
Ableitungsfunktionen von forge, verschlüsseltes PKCS#8 über `pkcs8Iterations` aus den Parametern),
zu teuer ergibt `protectionTooExpensive` für diese Datei. Der PEM-Scanner `pem-scan.ts` ist linear
(Review CR-02): die alte Regex `BEGIN … END` war bei vielen BEGIN-Zeilen ohne END quadratisch und
blockierte die API (5 MiB: Minuten); der Scanner sammelt die END-Stellen je Etikett in einem
Durchlauf und begrenzt die Blöcke auf 1000 je Text. Stellen, die Lesefehler bewusst verschlucken,
reichen die Anfragegrenzen mit `rethrowRequestError` weiter. Beim Empfang zählt
`cert-upload.ts` (eigener multer-Speicher) die Summe mit und bricht bei 20 MiB mit 413 `tooLarge` ab,
statt erst nach 30 vollständig gepufferten Dateien (Review WR-06). Die HTTP-Einrichtung
(`http-setup.ts`, aus `main.ts`) hängt den `build`-Leser **hinter** `enableCors` ein, damit auch
seine 413/400-Antworten CORS-Kopfzeilen tragen (Review WR-01; das Spec prüft die Reihenfolge).
*Grenze des Anfragekörpers von `build` (D-26).* Die Express-Voreinstellung von 100 kB reicht für die
größte erlaubte `build`-Anfrage nicht (Zertifikat, 20 Pool-Zertifikate, Schlüssel und Anfrage zu je
16 384 Zeichen ergeben rund 384 kB). Deshalb hat nur diese Route einen eigenen JSON-Leser mit
512 KiB (`cert-json-body.ts`), den `main.ts` per `app.use(CERT_BUILD_ROUTE, certBuildJsonBody,
certBuildBodyErrors)` **vor** `app.listen` einhängt (Nest registriert seine eigenen Leser erst in
512 KiB (`cert-json-body.ts`), den `configureHttp` in `http-setup.ts` per `app.use(CERT_BUILD_ROUTE,
certBuildJsonBody, certBuildBodyErrors)` nach `enableCors` und **vor** `app.listen` einhängt (Nest registriert seine eigenen Leser erst in
`init()`). Zwei Fallen: Erstens liegt `express` nicht in `apps/api/node_modules`, der Leser kommt
deshalb über `createRequire(...)` aus der Kopie, die auch `@nestjs/platform-express` lädt. Zweitens
**darf die Funktion nicht `jsonParser` heißen**: Nests `ExpressAdapter` überspringt seinen eigenen