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

216 lines
25 KiB
Markdown

---
phase: quick-261009-ikt
reviewed: 2026-10-09T16:45:00Z
depth: standard
files_reviewed: 62
files_reviewed_list:
- apps/api/src/cert-manager/zip-expand.ts
- apps/api/src/cert-manager/cert-json-body.ts
- apps/api/src/main.ts
- apps/api/src/cert-manager/cert-manager.controller.ts
- apps/api/src/cert-manager/cert-manager.module.ts
- apps/api/src/cert-manager/cert-analyze.ts
- apps/api/src/cert-manager/cert-model.ts
- apps/api/src/cert-manager/cert-chain.ts
- apps/api/src/cert-manager/cert-pkcs12.ts
- apps/api/src/cert-manager/cert-keys.ts
- apps/api/src/cert-manager/cert-csr.ts
- apps/api/src/cert-manager/cert-names.ts
- apps/api/src/cert-manager/cert-output.ts
- apps/api/src/cert-manager/cert-templates.ts
- apps/api/src/cert-manager/cert-aia.ts
- apps/api/src/cert-manager/cert-types.ts
- apps/api/src/cert-manager/cert-manager.changelog.ts
- apps/api/src/cert-manager/dto/cert-build.dto.ts
- apps/api/src/cert-manager/dto/cert-fetch-issuer.dto.ts
- apps/api/src/common/public-url-guard.ts
- apps/web/src/app/(portal)/modules/cert-manager/actions.ts
- apps/web/src/app/(portal)/modules/cert-manager/working-set.ts
- apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts
- apps/web/src/app/(portal)/modules/cert-manager/page.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/FilesTab.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/AnalyzeTab.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/ChainView.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/TemplatesTab.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/PfxOptions.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/PasswordInput.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/ItemCard.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
findings:
critical: 3
warning: 7
info: 5
total: 15
status: issues_found
---
# Quick 261009-ikt: Code Review Report (Zertifikat-Manager Umbau)
**Reviewed:** 2026-10-09
**Depth:** standard (plus targeted reproductions in the scratchpad; no source files modified)
**Status:** issues_found
## Summary
Test suites are green (API cert-manager and common: 331 tests; web cert-manager: 142 tests; `tsc` clean; message gate "messages ok 237"; Biome only reports two pre-existing, untouched files). That does not cover the findings below, which I reproduced against the real code and libraries.
Verified as sound: the SSRF design in `cert-aia.ts` (the connect-time lookup really blocks a hostname that resolves to loopback, reproduced with a local server; the address is read from the certificate on the server; redirects are re-checked; the `isPublicHttpUrl` signature is unchanged, so favorites icon discovery and nextcloud-status are not broken, the hardening only blocks more); `cert-json-body.ts` is named so that Nest keeps its global JSON parser (`jsonParser` name check in `registerParserMiddleware`) and mounted without a global prefix; passwords and keys are never logged (the request log is path/status only; error texts are fixed strings); chain building requires `checkIssued` and `verify`, with cycle protection; template snippets never contain a real password (IIS uses `Read-Host`, Tomcat uses the placeholder `IHR-PASSWORT`); the `forge.pki` patch is synchronous and restored in `finally`.
Three problems are blockers: a decompression bomb that bypasses every ZIP limit, a quadratic PEM scanner that freezes the API process, and a PKCS#12 password-encoding bug that produces unusable "Modern" PFX files for passwords with umlauts and rejects correct umlaut passwords on read.
## Critical Issues
### CR-01: ZIP bomb bypasses all limits when the central directory declares size 0
**File:** `apps/api/src/cert-manager/zip-expand.ts:102-122` (root cause in adm-zip `methods/inflater.js`)
**Issue:** All limit checks run on the declared `entry.header.size` from the central directory. adm-zip 0.6.0 only passes `maxOutputLength` to zlib when the declared size is `> 0` (`expectedLength > 0 ? { maxOutputLength: expectedLength } : {}`). An entry that declares `size = 0` but carries a deflate stream of, say, 300 KB therefore:
- passes the ratio check (`0 / compressedSize = 0`),
- adds 0 to `total`,
- is inflated without any bound by `entry.getData()` (line 122). Only afterwards `data.length > maxEntryBytes` is checked, and the CRC check fails after the allocation has happened.
Reproduced: a 305,870-byte ZIP with the size field patched to 0 inflated to 314,572,800 bytes in 1.3 s. The upload limit is 5 MiB per file, and deflate reaches about 1000:1, so one request can allocate around 5 GB and crash or freeze the whole API container (an authenticated module user is enough). The header comment in the file ("adm-zip entpackt hoechstens die deklarierte Groesse") is wrong for this case. The comment's claim that "all checks run before the first unpacking" holds only for honest headers.
**Fix:** Do not rely on the header size for the bound. Reject the inconsistent case and inflate with your own cap:
```ts
if (size === 0 && entry.header.compressedSize > 0) {
ignored.push({ file, path, reason: 'suspicious' });
continue;
}
// and for the actual extraction use a hard cap instead of entry.getData():
// zlib.inflateRawSync(rawCompressedBytes, { maxOutputLength: limits.maxEntryBytes + 1 })
// (STORED entries: compare compressed length with maxEntryBytes first)
```
Add a spec that patches the size field to 0 (the repro is a five-line script with `buf.writeUInt32LE(0, centralDirOffset + 24)`).
### CR-02: Quadratic PEM block regex lets one small upload freeze the API for minutes
**File:** `apps/api/src/cert-manager/cert-model.ts:61` and `:271` (same regex in `cert-aia.ts:162,185`)
**Issue:** `/-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/g` is applied to the whole file text. For every `-----BEGIN X-----` that has no matching END, the lazy `[\s\S]*?` scans to the end of the input, so a file made of repeated BEGIN lines costs O(n^2). Measured with `detectBlob` on `'-----BEGIN A-----\n'` repeated: 100 KiB takes 0.38 s, 200 KiB takes 1.3 s, 400 KiB takes 5.0 s. The upload limit is 5 MiB per file (the client limit is 10 MiB total), which extrapolates to roughly 15 minutes of blocked event loop on a single request. Node is single threaded, so the whole Tessera API (login, dashboard, all modules) stops. The AIA copy is capped at 256 KiB (about 2 s) but is fed by an attacker-chosen public server, three URLs per click.
**Fix:** Replace the regex with a linear scanner (`indexOf('-----BEGIN ')`, read the label, `indexOf('-----END label-----', start)`, and abort or skip when no END exists so that a missing END is not rescanned for every later BEGIN), and add a block-count cap (for example 200 blocks per blob). Add a timing-style spec with 400 KiB of BEGIN lines.
### CR-03: "Modern (AES-256)" PFX is unreadable for non-ASCII passwords; correct umlaut passwords are rejected on read
**File:** `apps/api/src/cert-manager/cert-pkcs12.ts:113` (read) and `:185` (write)
**Issue:** forge derives the MAC and the PKCS#12 3DES keys from the UTF-16 password (correct), but the PBES2 key derivation (`algorithm: 'aes256'`, and every OpenSSL-3-default PFX on read) uses forge's PBKDF2 on the raw JS string, i.e. one byte per UTF-16 unit, instead of the UTF-8 bytes OpenSSL, Windows and Java use. Reproduced with `openssl 3`:
- Write: `writePkcs12({profile:'modern', password:'pässwörd'})` and `'pw€'` round-trip inside Tessera, but `openssl pkcs12 -in file -passin pass:pässwörd` fails with `bad decrypt`. The "compat" files open fine. The user downloads a PFX that no other tool can open, with the password they typed, and gets no error.
- Read: an OpenSSL-3 PFX created with `-passout 'pass:pässwörd€'` returns `{ ok:false, reason:'passwordWrong' }` for the correct password ("Das Passwort passt nicht"), because the MAC passes and the shrouded key bag then throws a decrypt error that the broad regex at `:120` classifies as a password problem.
Passwords with umlauts are realistic for this (German) user base. The same module's key export/import (`cert-keys.ts`) goes through node:crypto and is not affected.
**Fix:** Do not let forge decrypt or encrypt the PBES2 bag. For writing: build the shrouded key bag from `key.export({type:'pkcs8',format:'der',cipher:'aes-256-cbc',passphrase})` (node uses UTF-8) and keep forge only for the MAC and the cert bags. For reading: lift each `pkcs8ShroudedKeyBag` and decrypt it with `createPrivateKey({key: der, format:'der', type:'pkcs8', passphrase})`. As a minimum, reject non-ASCII passwords for `modern` with a clear error and, on read, try `forge.util.encodeUtf8(password)` as a second candidate. Add a round-trip spec against `openssl pkcs12` with a non-ASCII password for both profiles.
## Warnings
### WR-01: 413/400 answers of the build parser carry no CORS headers
**File:** `apps/api/src/main.ts:24-26` (cors is enabled later at `:36`)
**Issue:** `app.use(CERT_BUILD_ROUTE, certBuildJsonBody, certBuildBodyErrors)` is registered before `app.enableCors(...)`. In the Express adapter `enableCors` is just another `use()`, so middleware order is: request log, cookies, build parser (and its error handler), cors, Nest parsers. For an oversized or malformed build body, `certBuildBodyErrors` answers 413/400 before the cors middleware ran, so in the cross-origin setup (`NEXT_PUBLIC_API_URL=http://localhost:3001`) the browser blocks the response and `fetch` throws a `TypeError`. The web code then shows `errors.generic` instead of the intended `tooLarge`/`invalidInput` messages. Same-origin production is unaffected.
**Fix:** Register the build parser after `enableCors` (still before `listen`):
```ts
app.enableCors({ origin: corsOrigin, credentials: true });
app.use(CERT_BUILD_ROUTE, certBuildJsonBody, certBuildBodyErrors);
```
Parsers are only installed at `init()` inside `listen`, so the order relative to Nest's global parser stays correct.
### WR-02: Output tabs keep working on a stale analysis (removed files, failed re-analysis)
**File:** `apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts:67-81`, consumers `MergeTab.tsx:42-56`, `TemplatesTab.tsx:71-85`, `ConvertTab.tsx:59-70`, `SplitTab.tsx:71-80`
**Issue:** `remove()`, `clear` of single entries and password changes update `entries` immediately but leave the previous `analysis` in place until the new response arrives. If the re-analysis fails (network, 413, 500) the old `analysis` stays indefinitely with `status:'error'`. The tabs only look at `status === 'analyzing' && !analysis`, so they continue to offer downloads of certificates and private keys from a file the user just removed (for example a key file removed for safety). The "N Dateien" line then no longer matches what is offered.
**Fix:** Treat the analysis as valid only when it matches the current entries: expose `fresh = status === 'idle' && analysisIds.length === entries.length && analysisIds.every((id, i) => id === entries[i].id)` from the hook, show the "analysing" or error state otherwise, and disable the download buttons while `!fresh`.
### WR-03: "Aufteilen" silently writes password-protected keys in clear text
**File:** `apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx:36-47,95-105`, `apps/api/src/cert-manager/cert-keys.ts:111-124`
**Issue:** `keyItemFromObject` always exports an unencrypted PKCS#8 PEM, even when the source key was encrypted (`wasEncrypted: true`). The Split tab downloads that PEM as `schluessel.key` and puts every key unencrypted into the "alle als ZIP" archive. A user who splits a protected bundle gets plaintext key files with no hint (the intro text `split.intro` says nothing), and the unlocked key also travels in every analyze response. This is a security-relevant surprise for a tool whose purpose is handling secrets.
**Fix:** In Split, show a visible note on keys with `wasEncrypted`, or offer the same password option as the Convert tab (`exportKey(key,'pkcs8',password)`), or exclude keys from the ZIP unless the user ticks them. At minimum extend `split.intro` (de/en).
### WR-04: Attacker-chosen KDF iteration counts run synchronously, up to 11 times per PFX
**File:** `apps/api/src/cert-manager/cert-pkcs12.ts:105-122`, `apps/api/src/cert-manager/cert-keys.ts:155-163`
**Issue:** `readPkcs12` tries own password, empty password and up to 10 more, each with a full pure-JS forge PBKDF/MAC computation; the iteration count comes from the uploaded file and is never bounded. `openWithPasswords` does the same with node:crypto for encrypted PKCS#8 (synchronous OpenSSL PBKDF2). A crafted PFX or encrypted key with a huge iteration count blocks the event loop for as long as the author wants, per attempt, per file (30 files).
**Fix:** Parse the MAC iteration count (`macData.iterations`) and the PBES2/PBE iteration counts before trying passwords and treat anything above a sane cap (for example 1,000,000) as unsupported (`ignored: unsupportedKey` or a new reason). Keep the attempts per container low.
### WR-05: No cap on the number of parsed items; chain building is O(n^2) on the event loop
**File:** `apps/api/src/cert-manager/cert-chain.ts:27-52`, `apps/api/src/cert-manager/cert-analyze.ts:51-77`
**Issue:** The ZIP path limits entries (100), but a single PEM bundle may contain thousands of distinct certificates (5 MiB is about 10k certificates). `toNodes` runs `checkIssued` over every pair; `matchKeys` multiplies certificates by keys; `isSelfSigned` verifies a signature per certificate. Measured: 600 distinct CA certificates in one file take 480 ms; the cost grows quadratically, so a 10 MiB request blocks the API for minutes.
**Fix:** Cap items per request (for example 200 certificates and 50 keys; return `tooLarge` or an `ignored` reason beyond that), and precompute subject/issuer buckets so only certificates with a matching name are compared.
### WR-06: Multer buffers up to 150 MiB before the 20 MiB total check
**File:** `apps/api/src/cert-manager/cert-manager.controller.ts:62-76`
**Issue:** `FilesInterceptor('files', 30, { limits: { fileSize: 5 MiB } })` uses memory storage; the total limit (`CERT_MAX_TOTAL_BYTES`) is only checked after all 30 files are fully buffered, so a request can hold 150 MiB (plus multer parts overhead) before it is rejected. There are also no `fields`/`parts` limits.
**Fix:** Add `limits: { fileSize, files: 30, fields: 5, parts: 40 }` and check the running total early with a small custom multer storage or a `Content-Length` pre-check (reject above about 21 MiB) in a guard/middleware.
### WR-07: Certificates without basicConstraints are classified as server certificates, even self-signed CAs
**File:** `apps/api/src/cert-manager/cert-model.ts:115-131`
**Issue:** `role` is derived from `x.ca` (basicConstraints cA flag only). A self-signed certificate that lacks basicConstraints, including legacy v1 roots and some private CAs, gets `isCa=false` and therefore role `end-entity`, although `selfSigned` is true. Reproduced: a self-signed certificate without the extension gives `ca false, selfverify true, checkIssued true`. Consequences: such a root shows as "Serverzertifikat", appears as an extra chain head in `defaultHeads`, is never reported as `rootId`, and cannot be excluded by "Root-Zertifikat mitnehmen" (`cert-output.ts` filters by `role === 'root'`).
**Fix:** Treat `selfSigned && (x.ca || no basicConstraints but keyCertSign usage or v1)` as root, or at least `selfSigned && x.subject === x.issuer && !hasServerAuthSan` heuristics. Simplest consistent rule: if the certificate is self-signed and has no basicConstraints extension, show it as role `root` only when `keyUsage` contains keyCertSign or the version is 1; otherwise keep `end-entity` but do not offer it as a chain member.
## Info
### IN-01: IPv6 literal URLs are always rejected, so the literal branch of the IPv6 hardening is dead
**File:** `apps/api/src/common/public-url-guard.ts:162-166`
**Issue:** `URL.hostname` keeps the brackets (`[2606:4700::1111]`), so `isIP(url.hostname)` is 0, the code falls into `lookup('[...]')`, which fails and returns `false`. Safe (fail closed), but the changelog and header comment suggest literal IPv6 addresses are now parsed and judged; in practice only DNS answers reach `isPrivateIpv6`. Public IPv6 AIA/favicon hosts given as literals can never work.
**Fix:** Strip the brackets before `isIP` (`url.hostname.replace(/^\[|\]$/g, '')`) and add a spec for `new URL('http://[::ffff:7f00:1]/')` and a public literal.
### IN-02: `aiaInternal` text is used for port, credential and length refusals
**File:** `apps/api/src/cert-manager/cert-aia.ts:297-299,324-326`; message `errors.aiaInternal` in `de.json`
**Issue:** A public host with a non-default port (`http://pki.example.com:8080/ca.crt`) is refused with "verweist nicht auf einen öffentlichen Server", which is wrong. Also, the UI button is shown for such URLs because `cert-model.ts:aiaUrls` (line 90-104) does not apply the same filter as `issuerUrls` (credentials, length, port).
**Fix:** Add a separate code (for example `aiaPortNotAllowed`) or reword the message ("Adresse nicht zulässig"), and share one URL filter between `cert-model.ts` and `cert-aia.ts` so the button only appears when a fetch can actually happen.
### IN-03: `errors.generic` says "Dateien konnten nicht geprüft werden" for download failures
**File:** `apps/web/src/messages/de.json` (`certManager.errors.generic`), used by `MergeTab.tsx`, `ConvertTab.tsx`, `TemplatesTab.tsx` via `certErrorKey`
**Issue:** A 500 from `build` shows the analysis error text, which is misleading.
**Fix:** Add `errors.buildFailed` and map `generic` per call site, or phrase the generic message neutrally ("Die Anfrage konnte nicht ausgeführt werden.").
### IN-04: Object URL is revoked synchronously after `click()`
**File:** `apps/web/src/app/(portal)/modules/cert-manager/actions.ts:282-285`, `components/SplitTab.tsx:56-60`
**Issue:** `URL.revokeObjectURL(url)` directly after `anchor.click()` can cancel the download in some browsers (older Safari/Firefox).
**Fix:** `setTimeout(() => URL.revokeObjectURL(url), 1000)`.
### IN-05: Smaller UI accuracy and key issues in the Dateien tab and Split tab
**File:** `apps/web/src/messages/de.json` (`files.locked.note`), `FilesTab.tsx:268-273`, `SplitTab.tsx:95-105`
**Issue:** (a) The note says the password is "nur zum Öffnen der Datei übertragen", but every add/remove/unlock re-uploads all files with all passwords, and the server also tries each file's password on every other file (`candidatePasswords`). (b) The rejected list uses `key={`${r.name}-${r.reason}`}`, which duplicates when two files with the same name are rejected for the same reason. (c) Several keys in the Split tab get the identical file name `schluessel.key` for single downloads (only the ZIP is de-duplicated).
**Fix:** Reword the note ("wird mit jeder Prüfung mitgesendet, aber nie gespeichert"), use the index in the key, and name keys after their certificate (`certIds[0]`) or number them.
---
_Reviewed: 2026-10-09_
_Reviewer: Claude (gsd-code-reviewer)_
_Depth: standard_
## Fix status
Behoben in den lokalen Commits bfcf6d4 (API, Fixtures) und c5bffe9 (Web, Anleitungen). Jeder Blocker hat einen Regressionstest, der gegen den alten Code rot war: CR-01 vier Tests rot gegen die alte `zip-expand.ts`; CR-02 der Test mit 20 000 unvollständigen BEGIN-Zeilen brauchte mit der alten Regex 4,6 s; CR-03 vier Tests rot gegen das alte `cert-pkcs12.ts` (modern lesen und schreiben, mit `pässwörd` und `pw€`).
| ID | Status | Wie |
|----|--------|-----|
| CR-01 | fixed | `zip-expand.ts` entpackt selbst: `inflateRawSync` mit `maxOutputLength` (Einzelgrenze, höchstens Restgrenze der Summe), STORED vorab gegen den Deckel; Größe, Verhältnis und CRC am echten Ergebnis, abweichende Kopfgröße = `suspicious`, Summe der echten Größen > 20 MiB = ganzes ZIP `zipTooLarge`. Spec: ZIP mit Kopfgröße 0 (200 MiB Nullbytes) wird mit `tooLarge` abgewiesen, Arbeitsspeicher < 50 MiB, < 2 s; e2e live (200 MiB, Antwort in unter 5 s, API gesund). |
| CR-02 | fixed | Neu `pem-scan.ts` (END-Stellen je Etikett in einem Durchlauf, Zeiger je Etikett, 1000 Blöcke Deckel), benutzt von `cert-model.ts` und `cert-aia.ts`. Specs: 5 MiB BEGIN-Zeilen (gleiche und verschiedene Etiketten) weit unter 100 ms (bestes von drei Läufen), Wiederholungen ohne Etikett; `detectBlob` mit 5 MiB BEGIN-Zeilen; e2e live (< 2 s). |
| CR-03 | fixed | `withForgeKdf` in `cert-pkcs12.ts` ersetzt für die Dauer des synchronen Lesens und Schreibens `forge.pkcs5.pbkdf2` durch `node:crypto` mit UTF-8-Bytes (3DES/RC2/MAC-Ableitung bleibt BMPString wie bei OpenSSL; der Reviewer-Vorschlag mit Schlüsselbeutel über node:crypto war nicht nötig). Neue Fixtures `rsa-umlaut-*.pfx`, `rsa-euro-*.pfx` (OpenSSL 3.5, modern und compat). Specs: lesen aller vier mit richtigem Passwort, falsches bleibt `passwordWrong`, Schreiben: der Schlüsselbeutel öffnet sich unabhängig von forge mit `createPrivateKey` und UTF-8-Passwort. e2e live in beide Richtungen mit `openssl pkcs12 -passin pass:pässwörd` und `pw€`, compat und modern. Kein Passwort wird abgelehnt. |
| WR-01 | fixed | Neu `http-setup.ts` (`configureHttp`), `main.ts` ruft es auf; der build-Leser wird nach `enableCors` eingehängt. Spec prüft die Reihenfolge; e2e live: 413 mit `access-control-allow-origin` bei gesetztem Origin. |
| WR-02 | fixed | `useCertWorkspace` liefert `fresh` (keine Analyse läuft, letzte erfolgreich, gleiche Eintragskennungen in gleicher Reihenfolge). Die fünf Ausgabe-Reiter lesen nur noch `freshAnalysis(workspace)`; sonst `AnalysisPending` (Prüfhinweis oder Fehler mit „Erneut versuchen“). Dateien-Reiter zeigt weiter die letzte bekannte Analyse je Eintrag. Tests: Hook (`use-cert-workspace.test.tsx`) und alle fünf Reiter (`stale-analysis.test.tsx`); Browser dunkel geprüft (ikt-fix-*.png). |
| WR-03 | fixed | Aufteilen: war ein Schlüssel geschützt, ist „Schlüssel mit einem Passwort schützen“ vorgewählt, die Schlüsselknöpfe und die ZIP warten auf ein Passwort, die Ausgabe kommt von `build` (`content: key`, `pkcs8`) verschlüsselt, auch in der ZIP. Ohne Schutz sichtbarer Hinweis. Browser: heruntergeladene Datei ist `ENCRYPTED PRIVATE KEY`, mit dem neuen Passwort lesbar, mit dem alten nicht. Die Analyse-Antwort enthält den Schlüssel weiterhin entschlüsselt (wie von Task 4/5 für die Ausgabe vorgesehen, nirgends protokolliert). |
| WR-04 | fixed | `cert-budget.ts`: eine Ableitung höchstens 1 000 000 Runden, alle zusammen 6 000 000 je Anfrage, gemeldet vor dem Rechnen (PKCS#12 über die drei forge-Ableitungsfunktionen, PKCS#8 über `pkcs8Iterations` aus den Parametern); Folge: `protectionTooExpensive` für die Datei. Fixtures mit 2 000 000 Runden (PFX und PKCS#8); Specs und e2e. |
| WR-05 | fixed | `RequestBudget`: 200 Zertifikate, 50 Schlüssel, 50 Anfragen je Anfrage, über alle Dateien und ZIP-Einträge, geprüft beim Erzeugen (Fehler verschluckende Stellen reichen sie mit `rethrowRequestError` weiter); 413 `tooManyItems`, Meldung de/en. Specs (200 ok, 201 413, über Dateien und ZIP, 50/51 Schlüssel, Müll zählt nicht), e2e live mit 200/201 Zertifikaten. Die Zuordnung `matchKeys` und der Kettenbau bleiben damit begrenzt; eine eigene Vorsortierung nach Namen war dafür nicht mehr nötig. |
| WR-06 | fixed | Neu `cert-upload.ts`: eigener multer-Speicher zählt die Summe beim Empfang und bricht bei 20 MiB mit 413 `tooLarge` ab; dazu `limits` `files` 30, `fields` 5, `parts` 35, `fieldSize` 256 KiB. Specs mit Datenströmen; e2e live (5 Dateien zu 4,9 MiB). |
| WR-07 | fixed | `cert-model.ts` (`isCaCertificate`, wie OpenSSLs `X509_check_ca`): ohne basicConstraints zählt ein selbstsigniertes v1-Zertifikat oder eines mit keyCertSign als CA/Wurzel; mit basicConstraints entscheidet nur cA; ein selbstsigniertes Serverzertifikat ohne diese Erweiterung (nur digitalSignature/keyEncipherment) bleibt Serverzertifikat. Neue Fixtures (echte v1-Wurzel mit Blatt, v3 nur keyCertSign, v3 nur Server). Specs für Rolle, Kette (vollständig, Wurzel erkannt, kein zweiter Kopf), e2e. |
| IN-01 | fixed | `public-url-guard.ts` entfernt die Klammern vor `isIP`; Spec mit internen Schreibweisen in Klammern (gesperrt) und öffentlichen Literalen (erlaubt). |
| IN-02 | fixed | Neu `cert-aia-url.ts` (ein Filter für Anzeige in `cert-model.ts` und Abruf in `cert-aia.ts`: Protokoll, Zugangsdaten, Länge, Standardport); neuer Code `aiaNotAllowed` (Port, Zugangsdaten, Länge, Protokoll nach Weiterleitung, nur nicht abrufbare Adressen); `aiaInternal` bleibt für nicht öffentliche Server. Live-AIA (Let's Encrypt, YE2) unverändert grün. |
| IN-03 | fixed | `certErrorKey(error, 'buildFailed')` für Zusammenführen, Konvertieren, Vorlagen und Aufteilen; neue Meldung `errors.buildFailed`. |
| IN-04 | fixed | `downloadBase64` und der ZIP-Download widerrufen die Objektadresse nach 1 s. |
| IN-05 | fixed | (a) Passwort-Hinweis neu formuliert (wird bei jeder Prüfung mitgesendet, nie gespeichert), (b) Schlüssel der Liste abgewiesener Dateien mit laufender Nummer, (c) Schlüssel heißen nach ihrem Zertifikat, sonst `schluessel`, `schluessel-2` … |
Nicht geändert (Vorgabe): „Cert Manager“ in der Seitenleiste gegenüber „Zertifikat-Manager“ auf der Seite.
Messwerte der Gates: api vitest gesamt 169 Dateien / 3573 Tests grün; web vitest gesamt 160 Dateien / 1945 Tests grün; tsc api und web ohne Fehler; Biome auf allen geänderten Dateien ohne Befund; `check-cert-messages.cjs 237` meldet `messages ok 250`; `e2e-cert.sh all` (jetzt mit Abschnitt `review`) grün inkl. Live-Abruf bei ye2.i.lencr.org.