Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
168 KiB
phase, plan, type, wave, depends_on, quick_id, description, date, files_modified, autonomous, requirements, estimate, must_haves
| phase | plan | type | wave | depends_on | quick_id | description | date | files_modified | autonomous | requirements | estimate | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-261009-ikt | 01 | execute | 1 | 261009-ikt | Zertifikat-Manager (cert-manager) Umbau: ein Reiter „Dateien“ als gemeinsamer Arbeitsbereich (mehrere Dateien, ZIPs, eingefuegter PEM-Text), alle anderen Reiter arbeiten darauf; Fehler „zweite Datei ueberschreibt die erste“ behoben; Kettenbildung und Fullchain (Root waehlbar, Vorgabe ohne); alle gaengigen Formate rein und raus inkl. EC; Vorlagen fuer Zielsysteme; „Fehlendes Zertifikat holen“ (AIA, nur auf Knopfdruck, SSRF-sicher, gehaerteter gemeinsamer Adressschutz); Modulversion 1.2.0, Modul-Changelog, CHANGELOG und Anleitungen | 2026-10-09 |
|
true |
|
|
|
Purpose: user test 09.10. (.planning/todos/pending/2026-10-09-cert-manager-mehrere-dateien-zip-fullchain.md): merging accepted only one file (a second selection replaced the first), ZIPs had to be handled by hand, and there was no „give me the Fullchain“ choice. Research found the bigger gap: node-forge cannot read any EC certificate, so every ECDSA certificate (Let's Encrypt default, most modern CAs) failed or was silently dropped.
Eight tasks, strictly in order, each run by its own fresh executor, each ending with a green verify chain and exactly one commit, and each leaving a module that is usable on its own (only the tabs built so far are shown; no web code calls a missing route). Task 1 is the tracer: several files into one working set, one node:crypto parser for RSA and EC certificates, every certificate shown with its role per file — through every layer, proven live. Each further task adds one capability on top: Task 2 Zusammenführen (chain building, Fullchain and Nur Kette, root checkbox, own body limit for build), Task 3 vendor ZIP, PKCS#7, pasted text, Analysieren and Aufteilen, Task 4 keys, PFX and CSR with a password per file, Task 5 every output format with Konvertieren and the complete Zusammenführen, Task 6 templates plus version 1.2.0, changelogs and guides, Task 7 „Fehlendes Zertifikat holen“ with the hardened guard, Task 8 the full gates and the browser proof.
Locked decisions — from CONTEXT.md (user, NON-NEGOTIABLE):
- D-01 ONE upload tab, the first one („Dateien“): select or drop several files and/or ZIPs, paste PEM text. Everything forms a shared working set (list with remove buttons and what was recognised in each). Analysieren, Aufteilen, Zusammenführen, Konvertieren and Vorlagen work on that set and have no upload field of their own.
- D-02 Root certificate in Fullchain: selectable, default WITHOUT root, checkbox „Root-Zertifikat mitnehmen“.
- D-03 Missing intermediate: Tessera reports the gap clearly („Zwischenzertifikat fehlt“) and offers „Fehlendes Zertifikat holen“, which fetches the issuer from the certificate's AIA caIssuers URL — ONLY on button press, never automatically. SSRF-safe: http/https only, public addresses only via the project's
isPublicHttpUrlguard pattern, size and time limits, no redirects to private targets; the result is marked „nachgeladen“. - D-04 One-click templates for target systems: Nginx, Apache, Windows/IIS (PFX), Nginx Proxy Manager, plus (Claude's choice) HAProxy combined PEM and Tomcat/Java as PKCS#12. Java keystore (JKS) is skipped because it needs native tools. Free choice of content and format stays available (Konvertieren, Zusammenführen).
- D-05 All common formats as input AND output: PEM/CRT/CER (Base64), DER, PKCS#7 (.p7b/.p7c), PKCS#12 (.pfx/.p12 with password), private keys (PKCS#1, PKCS#8, encrypted/unencrypted, RSA and EC), CSR; outputs Fullchain, chain only (intermediates), single certificate, certificate plus key (PEM bundle), PFX with a chosen password. Chain building via issuer/subject plus key identifiers (here: OpenSSL's
checkIssued, which compares names, AKI/SKI and key usage, plus the real signature check), clear gap messages, verify that a private key matches its certificate. - D-06 The bug „second file overwrites the first“ disappears structurally (one append-only working set). The existing analysis behaviour (what each part is, validity, what belongs together, calm note for an unneeded locked PFX) is kept and its gaps (EC, keys, CSR, PFX bags) are closed.
Locked decisions — orchestrator answers to the research's open questions (NON-NEGOTIABLE):
- D-07 PFX encryption is user-selectable: default „Kompatibel (auch ältere Windows-Server)“ = forge
algorithm: '3des'(pbeWithSHA1And3-KeyTripleDES-CBC, HMAC-SHA1 MAC); option „Modern (AES-256)“ = forgealgorithm: 'aes256'(PBES2/PBKDF2/AES-256-CBC key bag; forge keeps the SHA-1 MAC). The IIS template (and the Tomcat template) preselect the compatible profile. - D-08 The old routes parse, split, merge, convert are removed together with
cert-manager.service.ts, its spec and the three old DTOs; the old export route is replaced by the newbuild,analyzekeeps its name with the new contract. ONE parser only: node:crypto (X509Certificate,createPrivateKey,createPublicKey,KeyObject.export) decides everything about certificates and keys; node-forge is used only for PKCS#12 and as a generic ASN.1 reader/writer (PKCS#7 walk and build, CSR walk). No production code calls forge's certificate, CSR or PKCS#7-message parsers (they are RSA-only). - D-09 Module version 1.2.0: a NEW top entry dated 2026-10-09 in
cert-manager.changelog.ts(1.1.0 from 2026-10-02 is released — latest Tessera release 1.10.1 on 2026-10-06). RootCHANGELOG.mdbullets go under „## Unveröffentlicht“. - D-10 Harden the shared SSRF guard
apps/api/src/common/public-url-guard.ts(isPrivateIpv6): IPv4-mapped addresses in hex form (::ffff:7f00:1), NAT6464:ff9b::/96, 6to42002::/16embedding a private IPv4 — with a new spec; mention it in CHANGELOG as a security fix. - D-11 Private keys and passwords are never persisted and never logged; the working set lives in browser memory only and is lost on reload — the „Dateien“ tab and the user guide say so.
- D-12 Docs are mandatory: Anwenderanleitung section rewritten for the new flow incl. templates and the AIA button; Betriebsanleitung notes (upload limits through Nginx Proxy Manager, outbound HTTP for AIA); Entwicklungsanleitung module paragraph. App texts German with „Sie“, de AND en messages.
- D-13 Proof: spec fixtures for RSA and EC (certificate, chains incl. cross-signed and same-name CA, encrypted PKCS#8, traditionally encrypted key, DER key, CSR, PFX in both output encryptions and as input in OpenSSL-3/compat/legacy form, P7B, vendor-like ZIP), openssl round trips in the e2e script, and a Playwright browser check (dark mode first, a few light) of the upload tab with ZIP and several files, Fullchain output, a template download and the missing-intermediate button.
Claude's discretion (decided here, apply as written):
- D-14 API surface:
@Controller('modules/cert-manager')with class-level@UseModule('cert-manager')(Benutzen level, global JwtAuthGuard and TenantGuard as before), exactly three POST routes, each@HttpCode(200):analyze(multipart, Task 1, passwords from Task 4),build(JSON, Task 2, extended in Tasks 5 and 6),fetch-issuer(JSON, Task 7). All stateless, nothing stored. No new npm package (node:crypto of Node 24, node-forge 1.4.0, adm-zip 0.6.0, undici 7 and fflate are already installed; no install step, no package checkpoint). - D-15 Analyze contract (in
cert-types.ts, mirrored 1:1 in webactions.ts):AnalysisResult { items: AnyItem[]; chains: ChainInfo[]; locked: LockedEntry[]; ignored: IgnoredEntry[] }.ItemSource { file: number (index of the uploaded file in request order); path: string (file name, or "zipname/entry/path" inside a ZIP; control characters removed, max 255) }.CertItem { id ('c-' + first 16 lowercase hex chars of sha256(DER)); kind 'certificate'; role 'end-entity' | 'intermediate' | 'root'; sources; pem (canonical PEM of the DER); baseName; cn; organization; issuerCn; issuerOrganization; notBefore; notAfter (ISO); isExpired; daysLeft; san: string[] (DNS names plain, others with prefix like 'IP:'); keyType ('RSA' | 'EC' | 'ED25519' | other upper-case name); keyBits: number | null; curve ('P-256' | 'P-384' | 'P-521' | raw name | null); serialNumber (upper-case hex); sha256 and sha1 (colon-separated upper-case hex, as Node prints); isCa; selfSigned; aiaIssuerUrls: string[] (http/https only, max 5); keyId: string | null; csrIds: string[] }.KeyItem { id ('k-' + 16 hex of sha256(SPKI DER)); kind 'privateKey'; sources; pem (unencrypted PKCS#8 PEM); baseName; keyType; keyBits; curve; wasEncrypted; certIds: string[] }.CsrItem { id ('r-' + 16 hex of sha256(DER)); kind 'csr'; sources; pem; baseName; cn; organization; san; keyType; keyBits; curve; keyId: string | null; certIds: string[] }.ChainInfo { headId; path: string[] (head first, then each issuer, root last when present); rootId: string | null; complete: boolean; gap: { certId; kind: 'afterLeaf' | 'afterCa'; missingIssuerCn; aiaUrls: string[] } | null; alternatives: number }.LockedEntry { file; path; container: 'pkcs12' | 'privateKey'; reason: 'passwordNeeded' | 'passwordWrong' }.IgnoredEntry { file; path; reason: 'unknown' | 'nestedZip' | 'encryptedZip' | 'brokenZip' | 'tooLarge' | 'suspicious' | 'zipTooLarge' | 'tooManyEntries' | 'unsupportedKey' }. Items are deduplicated by id (sources merged), ordered certificates (end-entity, intermediate, root; then by cn, then notAfter descending), keys, CSRs. Chain heads: every end-entity certificate; when the set has none, every certificate that issued no other certificate of the set. Fallback base names as today: 'zertifikat' (end-entity), 'ca', 'schluessel', 'anfrage';safeBaseNamekeeps its current rules (*.→wildcard., other characters →_, max 80). - D-16 Detection (one pipeline in
cert-model.ts, every detector wrapped in try/catch, a bad blob never fails the request): ZIP by magic bytesPK\x03\x04/PK\x05\x06(not by extension) →zip-expand.ts; text with-----BEGIN(BOM and CRLF tolerated, text around blocks ignored) → each block by label: CERTIFICATE / X509 CERTIFICATE, TRUSTED CERTIFICATE (leading certificate SEQUENCE only), PKCS7 / CMS, PRIVATE KEY / RSA PRIVATE KEY / EC PRIVATE KEY / ENCRYPTED PRIVATE KEY (incl.Proc-Type: 4,ENCRYPTED), CERTIFICATE REQUEST / NEW CERTIFICATE REQUEST; otherwise DER in this order: X.509 certificate → PKCS#12 (top-level SEQUENCE starting with INTEGER 3, tried with the passwords) → PKCS#7 signedData (OID 1.2.840.113549.1.7.2) → private key (pkcs8, pkcs1, sec1, then encrypted pkcs8 with the passwords) → CSR →ignored: unknown. PKCS#7 certificates are read by walking the ASN.1 withforge.asn1.fromDer(ContentInfo → [0] SignedData → certificates [0] SET) and handing each certificate's DER toX509Certificate. Task 1 implements the certificate stages, Task 3 ZIP and PKCS#7, Task 4 keys, PKCS#12 and CSR — all into the same pipeline. - D-17 Limits: multer
FilesInterceptor('files', 30, { limits: { fileSize: 5 MiB } }); the controller rejects a request whose files together exceed 20 MiB with 413tooLarge; the web refuses before upload more than 30 entries, a file over 5 MiB, a set over 10 MiB in total, pasted text over 256 000 characters, and an identical file (same name, size and lastModified) a second time. ZIP (ZIP_LIMITS, injectable for specs): one level only (an entry that is itself a ZIP →nestedZip), at most 100 non-junk entries (else the whole ZIP →tooManyEntries), junk skipped silently (directories,__MACOSX/, names starting with.,Thumbs.db,desktop.ini), declared size per entry ≤ 1 MiB (elsetooLarge), declared size / max(compressed, 1) ≤ 100 (elsesuspicious), encrypted entry (general-purpose flag bit 0) →encryptedZip, sum of declared sizes of the kept entries ≤ 20 MiB checked BEFORE any entry is inflated (else the whole ZIP →zipTooLarge), unreadable archive →brokenZip; entry names are used for display only and never written to disk; adm-zip inflates at most the declared size and checks the CRC. - D-18 Chains (
cert-chain.ts, pure functions;buildChainsin Task 2,matchKeysin Task 4; selfSigned and role below are per-certificate facts thatcertItemFromDercomputes from Task 1 on): issuers of C = every other certificate I withC.checkIssued(I) && C.verify(I.publicKey); selfSigned(C) = both against C itself; role = not CA → 'end-entity', CA and selfSigned → 'root', otherwise 'intermediate' (a self-signed non-CA stays end-entity with path [itself], complete true, rootId null, gap null). Path search: depth-first over verified issuers, depth ≤ 10, visited set; ranking of the found paths, in this order: ends at a self-signed certificate of the set first, fewer certificates that are expired or not yet valid, shorter, later notAfter of the first issuer, then sha256 ascending (deterministic);alternatives= number of other paths found (cap 10). An incomplete path ends at a certificate whose issuer is absent:gap.kind'afterLeaf' when that certificate is the head end-entity, else 'afterCa', with the issuer CN from the certificate and its caIssuers URLs. Key match:cert.checkPrivateKey(keyObject); CSR match: SPKI DER of the CSR equals the certificate's or the key's SPKI DER. Never match by name or modulus string. - D-19 Build contract (
POST build, JSON, own body limit 512 KiB per D-26,BuildOutputDtowith class-validator):{ content, format?, certPem? (≤ 16 384 characters), poolPems? (≤ 20, each ≤ 16 384 characters; an entry that is not a certificate → 400 notACertificate), keyPem? (≤ 16 384), csrPem? (≤ 16 384), includeRoot? (default false), includeChain? (default true, bundle only), password? (≤ 256), pfxEncryption? 'compat' | 'modern' (default compat), template? (Task 6), baseName? (≤ 120) }→{ files: [{ filename, content (base64), mimeType }], chainComplete: boolean, missingIssuerCn: string | null, snippet?: string | null }. The API always rebuilds the order withbuildChainsover certPem plus poolPems and uses the primary chain of certPem (certificates in the pool that do not belong to it are dropped). Matrix and file names (keep today's conventions): leaf — pem<base>.crt, der<base>.cer, p7b<base>.p7b(PEM PKCS#7), p7c<base>.p7c(DER PKCS#7); fullchain — pem<base>-fullchain.pem, p7b, p7c; chain — pem<base>-chain.pem, p7b, p7c (no intermediate and no included root → 400noChain); leafKey — pem<base>-bundle.pem(leaf, intermediates, root only with includeRoot, key last as unencrypted PKCS#8; includeChain false → leaf and key only); pfx —<base>.pfx(leaf, intermediates, root only with includeRoot, key when given; password required); key — pkcs8<base>.key(PRIVATE KEY or, with password, ENCRYPTED PRIVATE KEY AES-256-CBC), traditional<base>.rsa.key/<base>.ec.key(PKCS#1 / SEC1, with password AES-256-CBC Proc-Type), pkcs8-der<base>.key.der(with password encrypted PKCS#8 DER); traditional for other key types → 400formatNotPossible; csr — pem<base>.csr, der<base>.csr.der. PKCS#7 output is assembled withforge.asn1by hand (ContentInfo signedData, version 1, empty digestAlgorithms and signerInfos, encapContentInfo data, certificates [0] IMPLICIT with each certificate's own ASN.1), never through forge certificate objects. PEM outputs use Node'sX509Certificate#toString()blocks joined in path order with a trailing newline. - D-20 PKCS#12 (
cert-pkcs12.ts): reading via forgepkcs12FromAsn1— try the file's own password, then '' (empty password), then the other passwords of the request (distinct, max 10); certificate bags: DER =bag.asn1when present (EC certificates: forge leavesbag.certnull) else forge's encoding ofbag.cert; key bags (pkcs8ShroudedKeyBag and keyBag):bag.asn1→createPrivateKey({ format: 'der', type: 'pkcs8' })(EC keys: forge leavesbag.keyfalse). Writing: the research's Pattern 3 — inside ONE synchronous function temporarily replacepki.privateKeyToAsn1,pki.wrapRsaPrivateKeyandpki.certificateToAsn1with pass-through functions, callforge.pkcs12.toPkcs12Asn1(keyAsn1OrNull, certs, password, { algorithm, friendlyName: baseName, generateLocalKeyId: true }), restore the three originals infinally(the module is single-threaded and the call is synchronous, so no other caller can observe the patch); key DER comes fromKeyObject.export({ type: 'pkcs8', format: 'der' }), certificates leaf first. A header comment explains why. No hand-rolled PBE or MAC. - D-21 Templates (
cert-templates.ts, Task 6; all need leaf plus matching key, else 400templateNeedsKey; root only with includeRoot; key unencrypted): nginx → ZIP<base>-nginx.zipwithfullchain.pem,privkey.pem(PKCS#8), snippetssl_certificate /etc/nginx/ssl/<base>/fullchain.pem;andssl_certificate_key /etc/nginx/ssl/<base>/privkey.pem;; apache (2.4.8 and newer) →fullchain.pem,privkey.pem, snippetSSLCertificateFile …/fullchain.pem,SSLCertificateKeyFile …/privkey.pem; apache-legacy (older than 2.4.8) →cert.pem,chain.pem,privkey.pem, snippet withSSLCertificateFile,SSLCertificateKeyFile,SSLCertificateChainFile; iis → single<base>.pfx(password required, pfxEncryption default compat), snippetImport-PfxCertificate -FilePath .\<base>.pfx -CertStoreLocation Cert:\LocalMachine\My -Password (Read-Host -AsSecureString); npm (Nginx Proxy Manager, „Custom“ certificate) →certificate.pem(leaf only),intermediate.pem(intermediates, root only with includeRoot),privkey.pem(RSA as PKCS#1 „RSA PRIVATE KEY“, EC as SEC1 „EC PRIVATE KEY“, research A2), snippet null, steps from the web messages; haproxy → single<base>.pem= leaf, intermediates, key (PKCS#8), snippetbind :443 ssl crt /etc/haproxy/certs/<base>.pem; tomcat → single<base>.p12(password required, compat default, friendlyName = base name), snippet<Certificate certificateKeystoreFile="conf/<base>.p12" certificateKeystorePassword="IHR-PASSWORT" certificateKeystoreType="PKCS12" certificateKeyAlias="<base>" />(never the real password). The API returns the files and the snippet; the web zips multi-file templates with fflate and addsANLEITUNG.txt(de) /INSTRUCTIONS.txt(en) made of the localized steps and the snippet; single-file templates download directly; the snippet is always shown with „Kopieren“. - D-22 AIA fetch (
cert-aia.ts,POST fetch-issuerwithFetchIssuerDto { pem: string ≤ 16 384 }— whitelist strips any other field, a URL is never accepted from the browser): parse the PEM (notACertificate400 otherwise); URLs fromtoLegacyObject().infoAccess['CA Issuers - URI'](Node'sinfoAccessgetter is a plain string, verified), keep http/https, no username/password, length ≤ 2048, default port only (emptyurl.port), max 3 URLs tried in order; none → 422aiaMissing. Per URL: the loop ofnextcloud-logo-fetch.ts—isPublicHttpUrlbefore the first request and before every hop (refused → 422aiaInternal),redirect: 'manual', max 3 redirects with the same checks, one AbortController for 8 s plusPromise.raceon abort, content-length pre-check and streamed cap 256 KiB (aiaTooLarge502), no cookies, no credentials, no custom User-Agent,discard()non-final bodies; the undici fetch runs with an ownAgent({ connect: { lookup } })whose lookup (createGuardedLookup) resolves the name and fails when any address is private (closes the DNS-rebinding window for this feature). Response parse: DER certificate, else PKCS#7 DER/PEM (the D-16 walker), else PEM text; accept only certificates wheretarget.checkIssued(c) && target.verify(c.publicKey)(none → 422aiaNotIssuer); other failures → 502aiaUnreachable. Result{ filename (<safeBaseName(cn)>.crt), pem (the accepted certificates), host, cn }. One hop per click. Log one warn line with host and code on failure only — never the PEM or the URL path. - D-23 Web structure:
page.tsxholdsuseCertWorkspace()and rendersPageHeaderplusTabBarfrom@/components/accounting/tab-bar; tab ids and labels in this order: files „Dateien“ (with the count, e.g. „Dateien (3)“), analyze „Analysieren“, split „Aufteilen“, merge „Zusammenführen“, convert „Konvertieren“, templates „Vorlagen“ — Task 1 shows files, Task 2 adds merge, Task 3 analyze and split, Task 5 convert, Task 6 templates, always in the order above; start tab files; switching tabs keeps the set. Non-files tabs with an empty set renderEmptyWorkspace(„Noch keine Dateien. Laden Sie Ihre Zertifikate im Reiter „Dateien“ hoch.“ plus a button to that tab); with a set, a calm line „Grundlage: {count} Dateien aus dem Reiter „Dateien“.“.working-set.ts(pure) keepsWorkingEntry { id, file: File, label, origin: 'upload' | 'paste' | 'fetched', host: string | null, password: string }; pasted text becomespasted-<n>.pemwith the label „Eingefügter Text “; fetched certificates become entries with origin fetched and their host.use-cert-workspace.tsre-sends the whole set after every change and drops answers of older requests (request counter). Design: Mosaik tokens, calm dense list, existing role badge colours of the old Übersicht, visible focus, no ALL-CAPS labels, no arrow characters or arrow buttons, no middle-dot meta strings, texts formal „Sie“ with real umlauts, no tenant or licence words; every text throught()(namespacecertManager,certManager.titlestays becausenav-store.tsuses it). - D-24 Errors: the API throws Nest exceptions with an object body
{ code, message }(English message for developers):invalidInput400,notACertificate400,noChain400,keyMissing400,keyMismatch400,passwordRequired400,formatNotPossible400,templateNeedsKey400,tooLarge413,aiaMissing422,aiaInternal422,aiaNotIssuer422,aiaUnreachable502,aiaTooLarge502. The web mapscode(and status 413 without code) tocertManager.errors.<code>with German texts that say what to do, unknown → a generic text. Validation errors of the DTO arrive as Nest's default 400 →invalidInput. - D-25 Tests: committed fixtures under
apps/api/src/cert-manager/__fixtures__/(biome ignores this folder; specs read them withreadFileSync(join(__dirname, '__fixtures__', …))like the tenders specs) generated once bymake-fixtures.shwith the host openssl 3.5.7; CA keys stay in a temporary directory and are deleted, only leaf keys are committed; file names never end in.key(.gitignoreline 48 ignores*.keyfor the updater signing key — fixtures use-key.pem/-key-….der). Specs never call openssl (CI has none) — they round-trip through Node and forge; the e2e script uses openssl. One e2e scripte2e-cert.sh [files|fullchain|zip|inputs|formats|templates|version|aia|all]against the local API (each task adds its section;allruns every section built so far); the browser proof in Task 8. - D-26 Request body of
POST build(Claude's discretion; resolves the plan-checker finding that the DTO allowed far more than Express's default 100 kB JSON limit, so a valid request could fail with a 413 without code).buildgets its own JSON parser withCERT_BUILD_JSON_LIMIT = 512 * 1024bytes incert-json-body.ts, registered inmain.tsasapp.use(CERT_BUILD_ROUTE, certBuildJsonBody, certBuildBodyErrors)withCERT_BUILD_ROUTE = '/modules/cert-manager/build'(the API has no global prefix) aftercookieParser()and beforeapp.listen—app.useregisters at once, Nest registers its own parsers later ininit(). Size arithmetic: certPem 16 384 + 20 × 16 384 poolPems + keyPem 16 384 + csrPem 16 384 characters, password 256, baseName 120 and JSON-escaped PEM newlines (about +1.6 %) ≈ 384 kB < 512 KiB, so every body within the DTO caps is parsed and reaches validation; a bigger body gets 413{ code: 'tooLarge', message }and malformed JSON 400{ code: 'invalidInput', message }fromcertBuildBodyErrors; every other error goes tonext. All other routes keep Nest's default 100 kB;analyzeis multipart andfetch-issuercarries at most 16 384 characters. The parser is Express's ownjson()from the Express copy that Nest's adapter loads —createRequire(createRequire(__filename).resolve('@nestjs/platform-express'))('express'), typed viatypeof import('express');expressitself is not resolvable fromapps/apiand no package is added — wrapped in a function namedcertBuildJsonBody, because Nest'sExpressAdapter.registerParserMiddlewareskips its global JSON parser for EVERY route when a router layer whose function is namedjsonParseralready exists. body-parser 2.3.0 returns early for a request whose body was already read, so the global parser does not read the build body again.cert-json-body.spec.tsbuilds the maximal DTO body from the exported caps ofdto/cert-build.dto.tsand asserts it stays below the limit, so a later cap change cannot reopen the gap. The Entwicklungs- and Betriebsanleitung mention the limit (Task 6).
Output: new parser, chain, ZIP, output, PKCS#12, CSR, key, template and AIA modules with specs, the build body parser, hardened shared guard with spec, three routes, rebuilt web module (working set, six tabs), messages de/en, fixtures, e2e script and message gate, module version 1.2.0 with changelog, CHANGELOG, three guides, screenshots. Eight commits on main (one per task), NOT pushed.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
Discovered facts the executor can rely on (verified during planning on 2026-10-09):
- Current API module:
cert-manager.controller.ts(routes parse, split, merge, convert, analyze, export; class@UseModule('cert-manager')),cert-manager.service.ts(793 lines, forge),cert-bundle.ts(655 lines, forge;analyzeBundle,exportBundleItem,safeBaseName, ZIP expansion via adm-zip by.zipextension, 100 entries, 5 MiB per entry header size),cert-manager.module.ts(providers[CertManagerService], seeds on init and logs „Cert-Manager module seeded in registry“),cert-manager.seed.ts(version: latestVersion(CERT_MANAGER_CHANGELOG)),cert-manager.changelog.ts(top entry 1.1.0 dated 2026-10-02),dto/{convert-cert,merge-certs,parse-cert}.dto.ts(plain classes).CertManagerControllerappears in no other spec (not inmodule-manage-handlers.spec.ts);app.module.tsimportsCertManagerModuleandmodule-changelog.registry.tsmaps 'cert-manager' — both stay. - Current web module:
page.tsx(own tab nav, shared single-fileDropZone+ paste +PasswordFieldfor every tab except Übersicht — root cause of the bug), componentsOverviewTab,InspectTab,SplitTab(fflatezipSyncwith filename dedupe),MergeTab,ConvertTab,DropZone,PasswordField(hard-coded German aria-labels),actions.ts(API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'—/api-proxyin production,downloadBase64(filename, base64, mime),postForm, fetch withcredentials: 'include'),zip-filename.ts(sanitizeZipFilename,ZIP_FILENAME_FALLBACK = 'certificates.zip'; keep it),layout.tsx(ModuleAccessGate; untouched). The oldOverviewTab.tsx(card layout,ROLE_STYLES,formatDatein UTC, explanations) is the visual reference for Task 3's Analysieren tab — after Task 1 deleted it, read it withT1=$(git log --format=%H -1 --grep='Parser für RSA und EC'); git show "$T1^:apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.tsx"(the same way for the oldSplitTab.tsxandcert-bundle.ts). - Node 24 facts (probed on this host):
new X509Certificate(pemOrDer);x.infoAccessis a STRING ("CA Issuers - URI:http://ye2.i.lencr.org/"), the parsed form isx.toLegacyObject().infoAccess→{ 'CA Issuers - URI': ['http://ye2.i.lencr.org/'] };toLegacyObject().subject→{ CN: 'letsencrypt.org' }(values can be arrays for multi-valued attributes);x.publicKey.asymmetricKeyType'ec' / 'rsa',asymmetricKeyDetails{ namedCurve: 'prime256v1' }or{ modulusLength }(map prime256v1 → P-256, secp384r1 → P-384, secp521r1 → P-521);x.subjectAltNameis a string likeDNS:a, DNS:b;x.ca,x.fingerprint256,x.fingerprint,x.serialNumber,x.validFromDate/x.validToDate,x.checkIssued(other),x.verify(publicKey),x.checkPrivateKey(key),x.raw(DER),x.toString()(PEM). Live chain: the letsencrypt.org leaf (EC P-256) nameshttp://ye2.i.lencr.org/; that URL answers 200application/pkix-certwith 656 bytes DER of „CN=YE2“ (EC);leaf.checkIssued(ye2) && leaf.verify(ye2.publicKey)is true; YE2 nameshttp://ye.i.lencr.org/(issuer „Root YE“). The api container reaches it (docker compose exec -T api node -e "fetch('http://ye2.i.lencr.org/')…"→ 200). - Host tools: OpenSSL 3.5.7 (
pkcs12 -export -legacyworks,req -x509 -not_before 20200101000000Z -not_after 20210101000000Zworks),zip,unzip,python3. - Shared guard
apps/api/src/common/public-url-guard.ts:isPrivateIpv4,isPrivateIpv6(only::,::1, prefixes fc/fd/fe80:/ff and dotted::ffff:a.b.c.d),isPrivateIpAddress(NOT exported),isBlockedHostname,export async function isPublicHttpUrl(url: URL)(http/https, blocked names, literal IPs,dns/promiseslookup all). Users:favorites/icon-discovery.service.ts,nextcloud-status/nextcloud-logo-fetch.ts(and their specs); no guard spec exists.nextcloud-logo-fetch.tsis the loop to copy (optionsfetchImpl,isPublic,timeoutMs;discard(response); abort race; streamed byte cap; one warn line with host and code). - Body parser (probed 2026-10-09, basis of D-26): a JSON body over 100 kB is answered today with 413
{"statusCode":413,"message":"request entity too large"}(no code);require.resolve('express')fromapps/api→ MODULE_NOT_FOUND, whilecreateRequire(require.resolve('@nestjs/platform-express'))('express').jsonworks and returns a function namedjsonParser; Nest 11.1.27NestApplication.init()callsExpressAdapter.registerParserMiddleware, which skips json/urlencoded whenisMiddlewareApplied(name)finds a router layer whosehandle.nameequalsjsonParser/urlencodedParser; body-parser 2.3.0read()starts withif (onFinished.isFinished(req)) next(); in Vitest 3 ofapps/apiboth__filenameandrequireexist, and feeding a 5 000-byte body withcontent-lengththroughjson({ limit: 1000 })on aPassThroughwithheadersandmethodcalls back withstatus 413,type 'entity.too.large'; the api image copies the full pnpmnode_modules(apps/api/Dockerfilelines 40–42), so the same resolution works in the container;biome check apps/api/src/main.tspasses today. - NestJS: global
ValidationPipe({ whitelist: true, transform: true })inmain.ts; no body-parser options → Express JSON limit 100 kB for every route exceptbuildfrom Task 2 on (D-26);common/request-log.tslogs method, path (query cut), status, ms and user only. Metadata keys:MODULE_SLUG_KEY = 'moduleSlug'inmodule-registry/module.guard.ts; route metadata viaReflect.getMetadata('path', …),'method'(RequestMethod POST = 1) and'__httpCode__'(seenextcloud-files.controller.spec.tsfor the pattern). An object passed tonew BadRequestException({ code, message })becomes the response body. - Web: shared
TabBaratapps/web/src/components/accounting/tab-bar.tsx({ tabs: { id, label }[], active, onChange }, buttons witharia-current="page");PageHeaderfrom@/components/layout/page-header(moduleSlug,title,description); tests use Vitest 4 + Testing Library; prefer rendering withNextIntlClientProvider locale="de" messages={de} timeZone="Europe/Berlin"(asnextcloud-files-page.test.tsx) so missing keys fail; mock./actionswithvi.mockkeepingdownloadBase64spied.messages/umlaut-guard.spec.tsfails on new tokens with ae/oe/ue/ss that are not inUMLAUT_ALLOWLIST(umlaut-dictionary.ts) — add correct German words there; no message key for the module exists in any parity spec, so the verify's node check enforces de/en parity. - Biome:
biome.jsonexcludes**/__fixtures__; baselinebiome checkalready fails on the untouchedlayout.tsx,zip-filename.test.tsandmodule-registry/module-changelog.spec.ts(import order/format) — do not reformat them;biome linton both module folders passes;de.json,en.json,public-url-guard.ts,cert-manager.changelog.ts,cert-manager.seed.tspassbiome checktoday and must keep passing. - Module changelog rules (
docs/anleitung-entwicklung.md„### Modulversion und Modul-Changelog pflegen“, ~line 299): new-item entry = minor bump; one jump per module between Tessera releases; items 1–2 sentences,de+en, kindsnew | changed | fixed, real umlauts, „Sie“, no replacement spellings like „fuer“/„Aenderung“, no tenant/licence words, plain text. Guard:apps/api/src/module-registry/module-changelog.spec.ts(seed version = top entry, strictly descending versions, real dates descending). Marktplatz readsGET /modules/changelog/:slug.CHANGELOG.mdstarts with „## Unveröffentlicht“ → „### Neu“ / „### Geändert“ / „### Behoben“; module bumps are mentioned like „Modulversion 1.1.0.“ (see the DKV bullet). - Guides:
docs/anleitung-anwender.md„### Zertifikat-Manager“ at line 150 (intro plus four bullets plus a formats paragraph, ends before „### Domaincheck“ at line 161);docs/anleitung-betrieb.mdchapter „## 3. Konfiguration“ with „### Dateien (Nextcloud)“ at line 186 (bullet withclient_max_body_size≥10mat line 192) and the table „### Fehlerbilder“ (line ~755);docs/anleitung-entwicklung.md„## Konventionen und Fallstricke“ (line 693, paragraphs „Titel (quick-id): …“, last one „Dateien (Nextcloud), Teilen (quick-261009-dkv):“ around line 800).docs/anleitung-administration.mdhas no cert-manager text and the module has no settings — it stays unchanged. - e2e harness
.planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/e2e-lib.sh(source it; no side effects):API(http://localhost:3001),E2E_TMP,e2e_fail,e2e_login <jar>(admin/admin123),e2e_status <jar> <method> <url> [json] [out],e2e_expect,e2e_contains,e2e_activate <jar> cert-manager,e2e_wait_health. Multipart upload withcurl -s -b <jar> -F "files=@<path>;filename=<name>" … $API/modules/cert-manager/analyze; JSON handling withpython3 -I. - Stack: db, api :3001, web :3000 (production build through
/api-proxy) and mailhog run; rebuild withdocker compose up -d --build api web(plainupdoes not rebuild). Login admin/admin123. Playwright MCP is configured (.mcp.json, chromium); screenshots go to.playwright-mcp/cert-manager/(gitignored). Dark mode via the theme button in the header (never by adding the class by script). Never judge the UI by calling fetch from inside the page — navigate and read the rendered page. - Git: commit ONLY the task's files:
git add <new files>,git rm <deleted files>, thengit commit -m "…" -- <every file of the task>. German subject with prefixfeat(cert-manager):, body ends withCo-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>. Never push (the user bundles pushes), never deploy to the test server, never read.envfiles.
Execution protocol for the eight tasks (read before starting your task):
- Tasks run strictly in order 1 → 8, each by its own fresh executor. Read the frontmatter, the objective with D-01 … D-26, this context and YOUR
<task>only; open the files your<read_first>names; read RESEARCH.md only in the sections your task names. - TDD: write or extend the specs and tests of your
<behavior>first, see them fail, then implement. - Before the verify chain:
docker compose up -d --build api web(plainupdoes not rebuild); the e2e setup waits for /health. Every chain ends withe2e-cert.sh all, i.e. every section built so far. - End with exactly one commit of exactly your task's files (new, changed, removed), message as your task names it. Never push (the user bundles pushes after Task 8), never deploy.
- After the commit append „## Task N“ to
.planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/261009-ikt-SUMMARY.md(create it if missing; never committed by an executor) with the measured gate table, deviations, threat status and the output items the<output>section assigns to your task.
Contract (per D-15, D-19, D-24). New cert-types.ts with every type of D-15 (KeyItem, CsrItem, ChainInfo and LockedEntry are filled from Tasks 2 and 4 on), BuildInput/BuildResult of D-19 and the CertErrorCode union of D-24 plus a small certError(code, status, message) helper that throws the matching Nest exception with body { code, message }.
One parser, certificates first (per D-08, D-16, D-18). New cert-model.ts: detectBlob(blob, ctx) runs the D-16 pipeline with every stage as a named function; in this task the certificate stages are real (PEM labels CERTIFICATE, X509 CERTIFICATE, TRUSTED CERTIFICATE with the leading certificate SEQUENCE only; DER X.509) and the ZIP, PKCS#7, key, PKCS#12 and CSR stages are named slots that recognise nothing yet, so a blob no stage recognises ends as ignored unknown (Task 3 fills ZIP and PKCS#7, Task 4 keys, PKCS#12 and CSR). certItemFromDer(der, source) builds a CertItem from node:crypto only (fields of D-15, subject and issuer via toLegacyObject(), caIssuers via toLegacyObject().infoAccess, curve mapping as in the context facts, safeBaseName, and the per-certificate facts selfSigned and role of D-18). Do not call forge's certificate, CSR or PKCS#7-message parsers anywhere outside specs — they are RSA-only; the verify gate greps for the names of those forge functions, so keep them out of comments too. Write cert-model.spec.ts first.
Base name (per D-15). New cert-output.ts with safeBaseName only (moved from cert-bundle.ts, rules unchanged) and cert-output.spec.ts with the moved cases; Task 2 adds buildOutput to the same file.
Facade, route, module (per D-08, D-14, D-17, D-24). New cert-analyze.ts: analyzeWorkingSet(files, passwords = []) — detect every blob, dedupe by id with merged sources, order per D-15, return AnalysisResult (chains from Task 2, keys, CSRs and locked from Task 4; empty arrays until then). Rewrite cert-manager.controller.ts: header comment (stateless, nothing stored, passwords and keys never logged, the D-14 route list with the task that adds each route), constructor without services, @Post('analyze') with @HttpCode(200), FilesInterceptor('files', 30, { limits: { fileSize: 5 * 1024 * 1024 } }), no files → invalidInput 400, files together over 20 MiB → tooLarge 413. Rewrite cert-manager.module.ts without providers (keep the seeding and its log line). Remove with git rm: cert-bundle.ts, cert-bundle.spec.ts, cert-manager.service.ts, cert-manager.service.spec.ts and the three old DTO files — the old routes and the old analyze contract go away in the same commit as the web code that called them. Write cert-analyze.spec.ts and cert-manager.controller.spec.ts (metadata per behavior; the 400 and 413 cases by calling the handler with fake files) first.
Web (per D-01, D-06, D-11, D-23, D-24). Rewrite actions.ts: keep API_URL and downloadBase64; mirror the D-15 and D-19 types; CertManagerRequestError(status, code); analyzeWorkingSet(entries) (multipart via toFormData, credentials include); no password ever in a URL or log. New working-set.ts (pure, D-17 web limits, the full D-23 entry shape incl. origin, host and password so Tasks 3, 4 and 7 only add functions) with working-set.test.ts. New use-cert-workspace.ts: entries state, addFiles(files) → rejected[], remove(id), clear(), retry(), analysis state (idle | analyzing | error), re-analysis of the whole set after each change with a request counter that drops older answers, empty set → analysis null without a call. Rewrite page.tsx per D-23 with the tab files only. New components/FilesTab.tsx: one large drop button (real button for click AND drop, hidden <input type="file" multiple> with accept .zip,.pem,.crt,.cer,.cert,.der,.ca-bundle,.chain,.p7b,.p7c,.pfx,.p12,.csr,.req,.txt plus key extensions, drops accept any file because detection is by content), hint text with the limits, rejected files with their reason, the entry list (<ul>, per entry: label, size, the recognised certificates from analysis.items whose sources point to this entry with role label and CN, its ignored lines with reason texts, remove button with aria-label „„{name}“ entfernen“), „Alle entfernen“, the always visible note per D-11 („Die Dateien bleiben nur in diesem Browserfenster. Tessera speichert nichts davon; nach dem Neuladen oder Schließen der Seite ist die Liste leer.“), analysing status and error with „Erneut versuchen“. Remove with git rm: DropZone.tsx, InspectTab.tsx, OverviewTab.tsx, OverviewTab.test.tsx, SplitTab.tsx, ConvertTab.tsx, PasswordField.tsx, MergeTab.tsx, MergeTab.test.tsx (Tasks 2, 3 and 5 write the new tabs). Rewrite cert-manager.test.tsx (page) and write FilesTab.test.tsx per behavior first. Messages: restructure namespace certManager in de AND en (keep title and description; keys for tabs, files, roles, ignored reasons and errors.<code> for every D-24 code — all codes now, later tasks add only their own texts), German with „Sie“ and real umlauts, no arrow or middle-dot characters, no tenant or licence words; extend UMLAUT_ALLOWLIST only with correct German words the guard asks for.
Message gate. Write .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/check-cert-messages.cjs (node, no dependencies, resolves the two message files relative to the repo root found via git rev-parse --show-toplevel): flattens certManager of de.json and en.json, exits 1 with key mismatch when the key sets differ, too few keys when de has fewer keys than the first argument, title missing without certManager.title, bad text: <value> for any value matching /mandant|tenant|lizenz|licens|→|·/i (write the arrow and the middle dot as escapes in the regex); otherwise prints messages ok <count>.
Live e2e (per D-13, D-25). Write .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh with the Write tool (bash, set -euo pipefail, sources ../../261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/e2e-lib.sh relative to its own directory, FIX = the fixtures folder relative to the repo root, header comment: test values only, reads no .env). Argument: one of files, fullchain, zip, inputs, formats, templates, version, aia or all; all runs every section implemented so far in exactly this order; an argument naming a section not yet implemented fails with „Abschnitt noch nicht gebaut“. This task implements setup and files; Tasks 2–7 each add their section and append it to all. Setup: wait for health, admin login, e2e_activate <jar> cert-manager, probe POST $API/modules/cert-manager/parse — anything but 404 fails with the hint „API-Container neu bauen: docker compose up -d --build api“. Section files: POST analyze (curl -s -b <jar> -F "files=@<path>;filename=<name>" …) with rsa-leaf.pem, rsa-inter.pem, ec-leaf.cer, ec-inter.pem, ec-root.pem and rsa-leaf.cer → 200, exactly five distinct certificates, rsa-leaf with two sources, the EC leaf with keyType EC and curve P-256, roles end-entity/intermediate/root as expected; POST analyze without files → 400 with code invalidInput; POST parse, split, merge, convert and export → 404 each. Print e2e cert files ok.
Rebuild, run, commit. docker compose up -d --build api web, wait for /health, run the verify chain. Commit feat(cert-manager): Reiter „Dateien“ mit mehreren Dateien und ein Parser für RSA und EC – Durchstich with exactly the files of this task (new, rewritten and removed). Do not push. Append „## Task 1“ to the SUMMARY with output item 1 (fixture list actually generated, openssl version).
test -f apps/api/src/cert-manager/cert-model.spec.ts && test -f apps/api/src/cert-manager/cert-output.spec.ts && test -f apps/api/src/cert-manager/cert-analyze.spec.ts && test -f apps/api/src/cert-manager/cert-manager.controller.spec.ts && test -f "apps/web/src/app/(portal)/modules/cert-manager/working-set.test.ts" && test -f "apps/web/src/app/(portal)/modules/cert-manager/components/FilesTab.test.tsx" && pnpm --filter @tessera/api exec vitest run src/cert-manager src/module-registry && pnpm --filter @tessera/web exec vitest run modules/cert-manager src/messages && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager" && pnpm exec biome check apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager/components" "apps/web/src/app/(portal)/modules/cert-manager/page.tsx" "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/working-set.test.ts" "apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts" "apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx" apps/web/src/messages/de.json apps/web/src/messages/en.json && node .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/check-cert-messages.cjs 15 && test ! -e apps/api/src/cert-manager/cert-manager.service.ts && test ! -e apps/api/src/cert-manager/cert-bundle.ts && test ! -e apps/api/src/cert-manager/dto/parse-cert.dto.ts && test ! -e "apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.tsx" && test ! -e "apps/web/src/app/(portal)/modules/cert-manager/components/DropZone.tsx" && test ! -e "apps/web/src/app/(portal)/modules/cert-manager/components/PasswordField.tsx" && ! grep -nE "@(Post|Get)('(parse|split|merge|convert|export)')" apps/api/src/cert-manager/cert-manager.controller.ts && test -z "$(grep -rlE 'certificateFromPem|certificateFromAsn1|certificationRequestFromAsn1|messageFromPem|messageFromAsn1' apps/api/src/cert-manager --include=*.ts | grep -v '.spec.ts$')" && FIXSTAT=$(git status --porcelain --ignored apps/api/src/cert-manager/fixtures) && ! printf '%s\n' "$FIXSTAT" | grep -q '^!!' && test -s apps/api/src/cert-manager/fixtures/ec-chain.p7b && test -s apps/api/src/cert-manager/fixtures/rsa-legacy.pfx && test -s apps/api/src/cert-manager/fixtures/aia-private-leaf.pem && docker compose ps --status running --services | grep -qx api && bash .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh all && echo "task1 ok"
<fails_when>non-zero exit of the chain and the last line task1 ok missing; the visible signal is one of: a test -f/test ! -e/test -s/grep gate stopping the chain without tool output (the api vitest config has passWithNoTests, so the test -f gates in front are what catch a filter matching no spec), vitest printing FAIL or a failed count in its Test Files line, tsc printing error TS, biome printing Found with errors, check-cert-messages printing key mismatch, too few keys, title missing or bad text, or e2e-cert.sh printing E2E FAIL:</fails_when>
Several files land in one append-only working set (a second file never replaces the first), RSA and EC certificates in PEM, DER and TRUSTED form are recognised by one node:crypto parser and shown with role and CN per file; the old routes, the old service, the old DTOs and the old web components are gone; specs, tsc, biome, the message gate and the live files section are green on the rebuilt stack; one commit on main, not pushed.
Output (per D-02, D-19). Add buildOutput(input) to cert-output.ts for content fullchain and chain in format pem (Task 5 adds every other content and format to the same function): parse certPem and poolPems with X509Certificate (anything that is not a certificate → notACertificate), run buildChains, take the primary chain of certPem, drop the root unless includeRoot, return D-19 file names, base64 content, mime application/x-pem-file, chainComplete and missingIssuerCn. Extend cert-output.spec.ts first.
DTO and body limit (per D-19, D-26). New dto/cert-build.dto.ts: exported caps CERT_PEM_MAX = 16384, CERT_POOL_MAX = 20, CERT_PASSWORD_MAX = 256, CERT_BASENAME_MAX = 120; BuildOutputDto with class-validator — content IsIn fullchain and chain (Task 5 widens it), format IsIn pem, certPem IsString MaxLength, poolPems IsArray ArrayMaxSize with each IsString MaxLength, includeRoot IsBoolean, baseName IsString MaxLength, all optional except content; header comment with the size arithmetic of D-26. New cert-json-body.ts per D-26 (exports CERT_BUILD_ROUTE, CERT_BUILD_JSON_LIMIT, certBuildJsonBody, certBuildBodyErrors; header comment explaining the 512 KiB, why the function must not be called jsonParser, and that express is reached through @nestjs/platform-express). Write cert-json-body.spec.ts first (fake request = a PassThrough with headers and method, as in the context facts). In apps/api/src/main.ts register app.use(CERT_BUILD_ROUTE, certBuildJsonBody, certBuildBodyErrors) directly after cookieParser() and before app.listen, with a one-line comment pointing to quick-261009-ikt D-26.
Route. @Post('build') with @HttpCode(200) and @Body() dto: BuildOutputDto → buildOutput; header route list updated. Extend the controller spec first.
Web (per D-01, D-02, D-23, D-24). actions.ts: buildOutput(input) (JSON, credentials include), error mapping incl. 413 without code → tooLarge. New components/ChainView.tsx (ordered path with role badge, CN, issuer and validity per step, a thin connecting line instead of arrow characters, gap row per D-18: afterLeaf „Zwischenzertifikat fehlt: „{name}““ as a warning, afterCa as a calm note that the certificate above, usually the root, is missing and is not needed for a Fullchain without root; Task 7 adds the fetch button to this row). New components/MergeTab.tsx per behavior (head choice, ChainView, root checkbox per D-02, buttons „Fullchain herunterladen“ and „Nur Kette herunterladen“, busy state, error texts). New components/EmptyWorkspace.tsx per D-23. page.tsx: tab merge. Write MergeTab.test.tsx and extend the page test first. Messages de AND en for every new text (rules as in Task 1).
Live e2e (per D-13, D-26). Add section fullchain (after files in all): POST build with {} → 400 (the probe that the api container carries this task; else fail with the rebuild hint); fullchain for ec-leaf with ec-root and ec-inter in the wrong order plus rsa-inter → 200, decode the file with python3 -I, exactly two certificates, the first subject is ec.example.test, openssl verify -CAfile ec-root.pem -untrusted <ec-inter> <first certificate> OK; includeRoot → three; chain → only ec-inter; RSA fullchain without root → chainComplete false and missingIssuerCn „Tessera Test Root RSA“; body limit: a DTO-valid body of about 380 kB (certPem ec-leaf plus 20 poolPems of 16 000 characters that are not certificates) → 400 with code notACertificate (never 413); a body over 600 KiB → 413 with code tooLarge; POST $API/auth/login with a 150 kB JSON body → 413 (the global 100 kB limit of every other route is unchanged), while the JSON login of the setup still works (Nest's global JSON parser is still active). Every business error body seen carries a code; DTO validation errors are Nest's default 400 (D-24). Print e2e cert fullchain ok.
Rebuild, run, commit. docker compose up -d --build api web, wait for /health, run the verify chain. Commit feat(cert-manager): Zusammenführen mit Fullchain und Nur Kette with exactly the files of this task. Do not push. Append „## Task 2“ to the SUMMARY.
test -f apps/api/src/cert-manager/cert-chain.spec.ts && test -f apps/api/src/cert-manager/cert-json-body.spec.ts && test -f "apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx" && pnpm --filter @tessera/api exec vitest run src/cert-manager src/module-registry && pnpm --filter @tessera/web exec vitest run modules/cert-manager src/messages && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager" && pnpm exec biome check apps/api/src/cert-manager apps/api/src/main.ts "apps/web/src/app/(portal)/modules/cert-manager/components" "apps/web/src/app/(portal)/modules/cert-manager/page.tsx" "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/working-set.test.ts" "apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts" "apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx" apps/web/src/messages/de.json apps/web/src/messages/en.json && node .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/check-cert-messages.cjs 25 && grep -q "certBuildJsonBody" apps/api/src/main.ts && test -z "$(grep -rlE 'certificateFromPem|certificateFromAsn1|certificationRequestFromAsn1|messageFromPem|messageFromAsn1' apps/api/src/cert-manager --include=*.ts | grep -v '.spec.ts$')" && docker compose ps --status running --services | grep -qx api && bash .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh all && echo "task2 ok"
<fails_when>non-zero exit of the chain and the last line task2 ok missing; the visible signal is one of: a test -f/grep gate stopping the chain without tool output, vitest printing FAIL or a failed count in its Test Files line, tsc printing error TS, biome printing Found with errors, check-cert-messages printing key mismatch, too few keys, title missing or bad text, or e2e-cert.sh printing E2E FAIL: (among others when the login of the setup fails because the global JSON parser was disabled, or when a body within the DTO caps is answered with 413)</fails_when>
Zusammenführen builds the chain itself (issuer and signature check, decoy refused, cross-signed and expired variants resolved deterministically) and downloads Fullchain or Nur Kette as PEM, root only when ticked; gaps are named; every DTO-valid build body is parsed, a bigger one gets 413 with code tooLarge, every other route keeps 100 kB; specs, tsc, biome and the live files and fullchain sections are green on the rebuilt stack; one commit on main, not pushed.
ZIP and PKCS#7 slots (per D-08, D-16). Fill the two slots of cert-model.ts: ZIP by magic bytes (not by extension) → expandZip, a ZIP inside a ZIP → nestedZip, the entries' blobs go through the same pipeline; PKCS#7 PEM (labels PKCS7 and CMS) and DER (signedData OID 1.2.840.113549.1.7.2) via the forge ASN.1 walk of D-16, each certificate's DER handed to certItemFromDer. Extend cert-analyze.ts for ZIP sources. Extend the specs first.
Web (per D-01, D-06, D-11, D-23). working-set.ts + use-cert-workspace.ts: addText(text). FilesTab.tsx per behavior (ZIP grouping by contained path, reason texts, paste area). New components/ItemCard.tsx and components/AnalyzeTab.tsx (layout, badge colours, UTC date formatting and explanation texts of the old OverviewTab from history, adapted to the new items and to EC; Task 4 adds key and CSR cards). New components/SplitTab.tsx per behavior (fflate pattern of the old SplitTab from history). page.tsx: tabs analyze and split. Write AnalyzeTab.test.tsx, SplitTab.test.tsx and the extended FilesTab, working-set and page tests first. Messages de AND en (rules as in Task 1).
Live e2e (per D-13, D-17). Add section zip (after fullchain in all): build a vendor ZIP in $E2E_TMP with python3 -I (ec-leaf as ServerCertificate.crt, ec-inter, ec-root, __MACOSX/._x, an inner ZIP, readme.txt); POST analyze with rsa-leaf.pem, rsa-inter.pem and the ZIP → 200, five distinct certificates, two chains, ignored contains nestedZip and unknown for readme.txt, no __MACOSX path anywhere; the same ZIP uploaded as bundle.dat → the same certificates; rsa-chain.p7c and ec-chain.p7b → three certificates each; fullchain built from the EC certificates taken out of the ZIP analysis → two blocks. Print e2e cert zip ok.
Rebuild, run, commit. docker compose up -d --build api web, wait for /health, run the verify chain. Commit feat(cert-manager): Hersteller-ZIP, PKCS#7, eingefügter Text, Analysieren und Aufteilen with exactly the files of this task. Do not push. Append „## Task 3“ to the SUMMARY.
test -f apps/api/src/cert-manager/zip-expand.spec.ts && test -f "apps/web/src/app/(portal)/modules/cert-manager/components/AnalyzeTab.test.tsx" && test -f "apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.test.tsx" && pnpm --filter @tessera/api exec vitest run src/cert-manager src/module-registry && pnpm --filter @tessera/web exec vitest run modules/cert-manager src/messages && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager" && pnpm exec biome check apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager/components" "apps/web/src/app/(portal)/modules/cert-manager/page.tsx" "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/working-set.test.ts" "apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts" "apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx" apps/web/src/messages/de.json apps/web/src/messages/en.json && node .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/check-cert-messages.cjs 40 && test -z "$(grep -rlE 'certificateFromPem|certificateFromAsn1|certificationRequestFromAsn1|messageFromPem|messageFromAsn1' apps/api/src/cert-manager --include=*.ts | grep -v '.spec.ts$')" && docker compose ps --status running --services | grep -qx api && bash .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh all && echo "task3 ok"
<fails_when>non-zero exit of the chain and the last line task3 ok missing; the visible signal is one of: a test -f/grep gate stopping the chain without tool output, vitest printing FAIL or a failed count in its Test Files line, tsc printing error TS, biome printing Found with errors, check-cert-messages printing key mismatch, too few keys, title missing or bad text, or e2e-cert.sh printing E2E FAIL:</fails_when>
A vendor ZIP (also without the .zip name), PKCS#7 in PEM and DER for RSA and EC and pasted PEM text join the working set; junk is skipped silently, nested, encrypted, oversized and suspicious entries are named with their reason; Analysieren shows chains and every certificate in detail, Aufteilen downloads every part or all as ZIP; specs, tsc, biome and the live files, fullchain and zip sections are green on the rebuilt stack; one commit on main, not pushed.
PKCS#12 reading (per D-20). New cert-pkcs12.ts with readPkcs12(der, passwords) → { certDers, keyObjects } or locked per D-20 (detection only for a top-level SEQUENCE whose first element is INTEGER 3; own password, then '', then the other passwords). writePkcs12 follows in Task 5. Write cert-pkcs12.spec.ts first.
CSR (per D-05, D-16). New cert-csr.ts: csrItemFromDer(der, source) walking CertificationRequestInfo with forge.asn1.fromDer — subject attributes via forge.pki.RDNAttributesAsArray (CN, O), SPKI DER → createPublicKey({ format: 'der', type: 'spki' }) for type and size, SAN dNSName and iPAddress from the extensionRequest attribute (OID 1.2.840.113549.1.9.14, extension 2.5.29.17); no signature check. Write cert-csr.spec.ts first.
Slots, matching, analyze, route (per D-15, D-16, D-18, D-24). Fill the key, PKCS#12 and CSR slots of cert-model.ts (PEM labels and the DER order of D-16; PKCS#12 certificates and keys become ordinary items with the PFX as source). Add matchKeys(certs, keys, csrs) to cert-chain.ts per D-18 (checkPrivateKey; SPKI DER equality for CSRs; never names or modulus strings). analyzeWorkingSet runs matchKeys, fills keyId/certIds/csrIds and reports locked entries. The controller reads the optional multipart field passwords (validated per behavior, never logged) and hands it over. Extend the specs first.
Web (per D-01, D-05, D-06, D-11, D-24). working-set.ts + use-cert-workspace.ts: setPassword(id, password) and the passwords array in toFormData, aligned with the files; passwords exist only in this state and the multipart body. New components/PasswordInput.tsx (label, value, show/hide with translated aria-labels, autoComplete="off"). FilesTab, ItemCard and AnalyzeTab per behavior. Tests first (FilesTab, working-set, AnalyzeTab). Messages de AND en (rules as in Task 1).
Live e2e (per D-11, D-13). Add section inputs (after zip in all): analyze the full fixture set of the behavior with a passwords array → 200 with the expected kinds, the RSA leaf with keyId, the EC leaf from ec-compat.pfx with keyId, the CSRs matched; rsa-modern.pfx without password → locked passwordNeeded, with falsch → passwordWrong; rsa-legacy.pfx and rsa-modern.bin with the password → certificates plus key; passwords set to nope → 400 invalidInput; docker compose logs api --since 10m contains neither Test-Pass-123 nor PRIVATE KEY. Print e2e cert inputs ok.
Rebuild, run, commit. docker compose up -d --build api web, wait for /health, run the verify chain. Commit feat(cert-manager): Schlüssel, PFX und CSR erkennen, Passwort je Datei with exactly the files of this task. Do not push. Append „## Task 4“ to the SUMMARY.
test -f apps/api/src/cert-manager/cert-keys.spec.ts && test -f apps/api/src/cert-manager/cert-pkcs12.spec.ts && test -f apps/api/src/cert-manager/cert-csr.spec.ts && pnpm --filter @tessera/api exec vitest run src/cert-manager src/module-registry && pnpm --filter @tessera/web exec vitest run modules/cert-manager src/messages && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager" && pnpm exec biome check apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager/components" "apps/web/src/app/(portal)/modules/cert-manager/page.tsx" "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/working-set.test.ts" "apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts" "apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx" apps/web/src/messages/de.json apps/web/src/messages/en.json && node .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/check-cert-messages.cjs 50 && test -z "$(grep -rlE 'certificateFromPem|certificateFromAsn1|certificationRequestFromAsn1|messageFromPem|messageFromAsn1' apps/api/src/cert-manager --include=*.ts | grep -v '.spec.ts$')" && docker compose ps --status running --services | grep -qx api && bash .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh all && echo "task4 ok"
<fails_when>non-zero exit of the chain and the last line task4 ok missing; the visible signal is one of: a test -f/grep gate stopping the chain without tool output, vitest printing FAIL or a failed count in its Test Files line, tsc printing error TS, biome printing Found with errors, check-cert-messages printing key mismatch, too few keys, title missing or bad text, or e2e-cert.sh printing E2E FAIL: (among others when the api log contains a fixture password or a private key)</fails_when>
Private keys (PKCS#1, PKCS#8, SEC1; PEM and DER; plain and encrypted), PFX files (OpenSSL-3, compatible and legacy; also in a ZIP and without extension) and CSRs of RSA and EC are recognised with a password per file and matched to their certificates; locked files ask for their password; no password or key reaches the api log; specs, tsc, biome and every live section so far are green on the rebuilt stack; one commit on main, not pushed.
Key export (per D-19). Add exportKey(keyObject, format, password?) to cert-keys.ts (cipher aes-256-cbc; traditional = pkcs1 for RSA, sec1 for EC, else formatNotPossible). Extend the spec first.
Outputs (per D-02, D-07, D-19). Extend buildOutput with every remaining content × format of D-19 in one switch with exhaustive checking: the hand-built PKCS#7 (forge.asn1, no forge certificate objects), the bundle, PFX via writePkcs12 (keyPem parsed with createPrivateKey, checkPrivateKey against the leaf → keyMismatch otherwise), key and CSR exports; mime types: pem application/x-pem-file, der application/pkix-cert, p7b/p7c application/x-pkcs7-certificates, pfx application/x-pkcs12, key application/x-pem-file or application/octet-stream, csr application/pkcs10. Widen BuildOutputDto per behavior. Extend cert-output.spec.ts and the controller spec first.
Web (per D-02, D-05, D-07, D-23, D-24). actions.ts: the full BuildInput. New components/PfxOptions.tsx, new components/ConvertTab.tsx, MergeTab extended, page.tsx tab convert — all per behavior. Write ConvertTab.test.tsx and extend MergeTab.test.tsx and the page test first. Messages de AND en (rules as in Task 1).
Live e2e (per D-07, D-11, D-13). Add section formats (after inputs in all): for RSA and EC build and check with openssl — leaf der (openssl x509 -inform DER), fullchain p7b and p7c (openssl pkcs7 [-inform DER] -print_certs lists the path in order), bundle (first certificate is the leaf, openssl pkey -pubout of the key equals openssl x509 -pubkey -noout of the leaf), pfx compat (openssl pkcs12 -info -noout -passin pass:Neu-Pass-2026 output contains pbeWithSHA1And3-KeyTripleDES-CBC) and pfx modern (contains AES-256-CBC), encrypted pkcs8 key (openssl pkey -passin pass:… -noout OK), traditional key (BEGIN RSA PRIVATE KEY / BEGIN EC PRIVATE KEY), csr der (openssl req -inform DER -noout -subject); docker compose logs api --since 10m contains neither Test-Pass-123, Neu-Pass-2026 nor PRIVATE KEY. Print e2e cert formats ok.
Rebuild, run, commit. docker compose up -d --build api web, wait for /health, run the verify chain. Commit feat(cert-manager): alle Ausgabeformate, Konvertieren, Bundle und PFX with exactly the files of this task. Do not push. Append „## Task 5“ to the SUMMARY with output item 2 (the openssl outputs that prove the PFX algorithms and the P7B/P7C order).
test -f "apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.test.tsx" && grep -q "writePkcs12" apps/api/src/cert-manager/cert-pkcs12.spec.ts && grep -q "exportKey" apps/api/src/cert-manager/cert-keys.spec.ts && pnpm --filter @tessera/api exec vitest run src/cert-manager src/module-registry && pnpm --filter @tessera/web exec vitest run modules/cert-manager src/messages && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager" && pnpm exec biome check apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager/components" "apps/web/src/app/(portal)/modules/cert-manager/page.tsx" "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/working-set.test.ts" "apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts" "apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx" apps/web/src/messages/de.json apps/web/src/messages/en.json && node .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/check-cert-messages.cjs 60 && test -z "$(grep -rlE 'certificateFromPem|certificateFromAsn1|certificationRequestFromAsn1|messageFromPem|messageFromAsn1' apps/api/src/cert-manager --include=*.ts | grep -v '.spec.ts$')" && docker compose ps --status running --services | grep -qx api && bash .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh all && echo "task5 ok"
<fails_when>non-zero exit of the chain and the last line task5 ok missing; the visible signal is one of: a test -f/grep gate stopping the chain without tool output, vitest printing FAIL or a failed count in its Test Files line, tsc printing error TS, biome printing Found with errors, check-cert-messages printing key mismatch, too few keys, title missing or bad text, or e2e-cert.sh printing E2E FAIL: (among others when openssl cannot read an output or the PFX info lacks the expected algorithm)</fails_when>
Every output of D-19 is produced for RSA and EC and read back by openssl in the e2e (both PFX profiles with the expected algorithm, P7B/P7C in order); Konvertieren converts any single item, Zusammenführen offers every chain format, certificate plus key and PFX; no password or key appears in the api log; specs, tsc, biome and every live section so far are green on the rebuilt stack; one commit on main, not pushed.
Web (per D-04, D-23). actions.ts: the template fields. New components/TemplatesTab.tsx per behavior (per template: title, what you get, steps from messages templates.<id>.steps; for Nginx Proxy Manager the steps name SSL Certificates, „Add SSL Certificate“, „Custom“ and which file goes into Certificate Key, Certificate and Intermediate Certificate; English export INSTRUCTIONS.txt). page.tsx: tab templates. Write TemplatesTab.test.tsx and extend the page test first. Messages de AND en (rules as in Task 1).
Version and changelogs (per D-09). In cert-manager.changelog.ts add above 1.1.0 the entry version 1.2.0, date 2026-10-09 (1.1.0 unchanged) with these items (de / en, adjust wording only if the guard spec demands): new „Ein gemeinsamer Reiter „Dateien“: Laden Sie mehrere Dateien und ZIP-Dateien auf einmal hoch oder fügen Sie PEM-Text ein. Alle anderen Reiter arbeiten mit dieser Liste.“ / „One shared “Files” tab: upload several files and ZIP files at once or paste PEM text. All other tabs work with this list.“; new „„Zusammenführen“ ordnet die Kette selbst und liefert Fullchain, nur die Kette, Zertifikat mit Schlüssel oder eine PFX-Datei; das Root-Zertifikat nehmen Sie nur auf Wunsch mit.“ / „“Merge” orders the chain by itself and delivers a full chain, the chain only, certificate with key or a PFX file; the root certificate is only included on request.“; new „Alle gängigen Formate rein und raus, auch Zertifikate und Schlüssel mit elliptischen Kurven (EC) und verschlüsselte Schlüssel. PFX-Dateien wahlweise kompatibel oder modern verschlüsselt.“ / „All common formats in and out, including elliptic-curve (EC) certificates and keys and encrypted keys. PFX files with compatible or modern encryption.“; new „Vorlagen für Nginx, Apache, Windows/IIS, Nginx Proxy Manager, HAProxy und Tomcat liefern die passenden Dateien mit einem Klick.“ / „Templates for Nginx, Apache, Windows/IIS, Nginx Proxy Manager, HAProxy and Tomcat deliver the right files with one click.“; fixed „Beim Zusammenführen ersetzt eine zweite Datei nicht mehr die erste.“ / „When merging, a second file no longer replaces the first one.“ (Task 7 adds the item for the missing-intermediate fetch). In CHANGELOG.md under „## Unveröffentlicht“: in „### Neu“ two bullets „Zertifikat-Manager: …“ (the shared Dateien tab with ZIP and pasted text and the browser-only list; Zusammenführen with Fullchain, Nur Kette, bundle and PFX with the root checkbox, the Kompatibel/Modern choice, Vorlagen; „Modulversion 1.2.0.“), in „### Behoben“ one bullet for the replaced second file and the EC certificates and keys that were not recognised. Plain words, „Sie“, no file names.
Guides (per D-11, D-12; everyday language, „Sie“, detailed, no tenant or licence wording). docs/anleitung-anwender.md „### Zertifikat-Manager“: replace the whole section body including its old introduction sentence — the six tabs and the working-set idea, Dateien (several files, ZIPs, pasted text, limits 30 files and 10 MB, what the list shows, passwords per file, „Die Liste bleibt nur in diesem Browserfenster …“), Analysieren, Aufteilen, Zusammenführen (Fullchain, Nur Kette, Zertifikat und Schlüssel, PFX with password and Kompatibel/Modern advice, „Root-Zertifikat mitnehmen“ off by default and when you need it, what „Zwischenzertifikat fehlt“ means), Konvertieren, Vorlagen (one sentence per template: what you get and where it goes), supported formats; Task 7 adds the paragraph on „Fehlendes Zertifikat holen“. docs/anleitung-betrieb.md: new „### Zertifikat-Manager“ after „### Dateien (Nextcloud)“ in chapter 3 (uploads go through /api-proxy, one analysis sends at most 10 MB, so the client_max_body_size of at least 10m from the Dateien section covers it; a single download request is at most 512 KiB; nothing is stored, no setting) and one row in „### Fehlerbilder“ (upload aborted with 413). docs/anleitung-entwicklung.md „## Konventionen und Fallstricke“: paragraph „Zertifikat-Manager, Arbeitsbereich und Ketten (quick-261009-ikt):“ (node:crypto decides, forge only for PKCS#12 and as ASN.1 tool and why; the stateless routes; chain rule checkIssued plus verify; the scoped PFX patch; ZIP limits; the per-route body limit of build from D-26 including the jsonParser name trap; fixtures in __fixtures__ with make-fixtures.sh, never .key names). docs/anleitung-administration.md stays unchanged (no settings).
Live e2e (per D-13). Add sections templates and version (after formats in all). templates: for the RSA set and the EC set build every template, decode the files with python3 -I and check with openssl per behavior (nginx openssl verify of fullchain, key matches leaf; iis openssl pkcs12 -info contains pbeWithSHA1And3-KeyTripleDES-CBC; npm key headers; haproxy first block is the leaf and a key is present; tomcat readable). version: GET $API/modules/changelog/cert-manager → 200, first release 1.2.0 dated 2026-10-09; the catalog lists cert-manager with version 1.2.0. Print one ok line per section.
Rebuild, run, commit. docker compose up -d --build api web, wait for /health, check the api log for „Cert-Manager module seeded in registry“, run the verify chain. Commit feat(cert-manager): Vorlagen für Zielsysteme, Modulversion 1.2.0 und Anleitungen with exactly the files of this task. Do not push. Append „## Task 6“ to the SUMMARY.
test -f apps/api/src/cert-manager/cert-templates.spec.ts && test -f "apps/web/src/app/(portal)/modules/cert-manager/components/TemplatesTab.test.tsx" && pnpm --filter @tessera/api exec vitest run src/cert-manager src/module-registry && pnpm --filter @tessera/web exec vitest run modules/cert-manager src/messages && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager" && pnpm exec biome check apps/api/src/cert-manager "apps/web/src/app/(portal)/modules/cert-manager/components" "apps/web/src/app/(portal)/modules/cert-manager/page.tsx" "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/working-set.test.ts" "apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts" "apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx" apps/web/src/messages/de.json apps/web/src/messages/en.json && node .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/check-cert-messages.cjs 80 && grep -q "version: '1.2.0'" apps/api/src/cert-manager/cert-manager.changelog.ts && grep -q "date: '2026-10-09'" apps/api/src/cert-manager/cert-manager.changelog.ts && awk '/^## Unveröffentlicht/{f=1;next} /^## /{f=0} f' CHANGELOG.md | grep -q "Zertifikat-Manager" && grep -q "Root-Zertifikat mitnehmen" docs/anleitung-anwender.md && ! grep -q "Ein Werkzeug rund um SSL/TLS-Zertifikate mit vier Reitern" docs/anleitung-anwender.md && grep -q "^### Zertifikat-Manager" docs/anleitung-betrieb.md && grep -q "quick-261009-ikt" docs/anleitung-entwicklung.md && test -z "$(grep -rlE 'certificateFromPem|certificateFromAsn1|certificationRequestFromAsn1|messageFromPem|messageFromAsn1' apps/api/src/cert-manager --include=*.ts | grep -v '.spec.ts$')" && docker compose ps --status running --services | grep -qx api && docker compose logs api 2>&1 | grep -q "Cert-Manager module seeded in registry" && bash .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh all && echo "task6 ok"
<fails_when>non-zero exit of the chain and the last line task6 ok missing; the visible signal is one of: a test -f/grep/awk gate stopping the chain without tool output (version, date, CHANGELOG bullet or guide section missing, or the old introduction sentence still present), vitest printing FAIL or a failed count in its Test Files line (module-changelog.spec included), tsc printing error TS, biome printing Found with errors, check-cert-messages printing key mismatch, too few keys, title missing or bad text, or e2e-cert.sh printing E2E FAIL:</fails_when>
Seven target-system templates download with one click (with snippet, IIS and Tomcat compatible by default); module version 1.2.0 with its changelog, the CHANGELOG bullets and the three guides describe everything built so far; specs, tsc, biome and every live section so far incl. version are green on the rebuilt stack; one commit on main, not pushed.
AIA fetch (per D-03, D-22, D-24). New cert-aia.ts with fetchIssuer(pem, opts = {}) and createGuardedLookup(resolve = dns.lookup) exactly per D-22 (header comment listing every protection and the accepted remainder: any authenticated module user can make the API send one GET to a public address named in a certificate they upload; the answer is only returned when it is a verified issuer certificate). The real path passes dispatcher: new Agent({ connect: { lookup: createGuardedLookup() } }) from undici. New dto/cert-fetch-issuer.dto.ts and @Post('fetch-issuer') with @HttpCode(200) in the controller (header route list updated). Write cert-aia.spec.ts first per behavior and extend the controller spec.
Web (per D-01, D-03, D-11). actions.ts: fetchIssuer(pem). working-set.ts/use-cert-workspace.ts: addFetched({ filename, pem, host }) (origin fetched; the same certificate twice is not added again). ChainView.tsx: gap row with the button per behavior (props onFetched, busy per gap, error text); MergeTab and AnalyzeTab pass the workspace's addFetched. FilesTab.tsx and ItemCard.tsx: the „nachgeladen von {host}“ marker. Tests first: ChainView.test.tsx, extended FilesTab and working-set tests. Messages de AND en (rules as in Task 1).
Changelogs and guides for this feature (per D-03, D-09, D-10, D-12). cert-manager.changelog.ts: add to the 1.2.0 entry the item new „Fehlt ein Zwischenzertifikat, holt „Fehlendes Zertifikat holen“ es auf Knopfdruck beim Aussteller.“ / „If an intermediate certificate is missing, “Fetch missing certificate” gets it from the issuer at the click of a button.“. CHANGELOG.md under „## Unveröffentlicht“: extend the Zusammenführen bullet with „Fehlendes Zertifikat holen“ only on click, and add one bullet „Sicherheit: …“ in plain words that the protection against fetching internal addresses (favourite icons, Nextcloud-Status logos and the new certificate fetch) now also recognises hidden spellings of internal IPv6 addresses. Anwenderanleitung: paragraph „Fehlendes Zertifikat holen“ (only on click, Tessera asks the issuer on the internet, the entry is marked nachgeladen, a second click may be needed for the next level). Betriebsanleitung „### Zertifikat-Manager“: the api container needs outbound http/https on ports 80 and 443 to the certificate issuers' addresses, otherwise the button reports „nicht erreichbar“; one row in „### Fehlerbilder“ (fetch reports nicht erreichbar). Entwicklungsanleitung paragraph of Task 6: the AIA guard with guarded lookup and the hardened shared guard.
Live e2e (per D-03, D-13). Add section aia (last in all): aia-private-leaf.pem → 422 aiaInternal; rsa-leaf-noaki.pem → 422 aiaMissing; a body { "pem": …, "url": "http://127.0.0.1/" } behaves like without url; live — unless CERT_E2E_OFFLINE=1 is set — fetch the letsencrypt.org leaf with openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org into $E2E_TMP, analyze it → gap afterLeaf with an http aia URL, POST fetch-issuer → 200, host ends with lencr.org, openssl verify -partial_chain -trusted <fetched> <leaf> OK, analyze leaf plus fetched → path of two with gap afterCa; docker compose logs api --since 10m contains no BEGIN CERTIFICATE. Print e2e cert aia ok.
Rebuild, run, commit. docker compose up -d --build api web, wait for /health, run the verify chain. Commit feat(cert-manager): Fehlendes Zertifikat holen, gehärteter Adressschutz with exactly the files of this task. Do not push. Append „## Task 7“ to the SUMMARY with output item 3 (live AIA result: host, fetched CN, what the next level showed — or why CERT_E2E_OFFLINE=1 had to be used).
test -f apps/api/src/common/public-url-guard.spec.ts && test -f apps/api/src/cert-manager/cert-aia.spec.ts && test -f "apps/web/src/app/(portal)/modules/cert-manager/components/ChainView.test.tsx" && pnpm --filter @tessera/api exec vitest run src/cert-manager src/module-registry src/common src/favorites src/nextcloud-status && pnpm --filter @tessera/web exec vitest run modules/cert-manager src/messages && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/cert-manager apps/api/src/common "apps/web/src/app/(portal)/modules/cert-manager" && pnpm exec biome check apps/api/src/cert-manager apps/api/src/common/public-url-guard.ts apps/api/src/common/public-url-guard.spec.ts "apps/web/src/app/(portal)/modules/cert-manager/components" "apps/web/src/app/(portal)/modules/cert-manager/page.tsx" "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/working-set.test.ts" "apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts" "apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx" apps/web/src/messages/de.json apps/web/src/messages/en.json && node .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/check-cert-messages.cjs 85 && awk '/^## Unveröffentlicht/{f=1;next} /^## /{f=0} f' CHANGELOG.md | grep -q "IPv6" && grep -q "Fehlendes Zertifikat holen" docs/anleitung-anwender.md && grep -q "Fehlendes Zertifikat holen" apps/api/src/cert-manager/cert-manager.changelog.ts && test -z "$(grep -rlE 'certificateFromPem|certificateFromAsn1|certificationRequestFromAsn1|messageFromPem|messageFromAsn1' apps/api/src/cert-manager --include=*.ts | grep -v '.spec.ts$')" && docker compose ps --status running --services | grep -qx api && bash .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh all && echo "task7 ok"
<fails_when>non-zero exit of the chain and the last line task7 ok missing; the visible signal is one of: a test -f/grep/awk gate stopping the chain without tool output (security bullet, guide paragraph or changelog item missing), vitest printing FAIL or a failed count in its Test Files line (favorites and nextcloud-status included), tsc printing error TS, biome printing Found with errors, check-cert-messages printing key mismatch, too few keys, title missing or bad text, or e2e-cert.sh printing E2E FAIL: (among others when a private address is contacted, the live letsencrypt fetch fails or the api log contains a certificate)</fails_when>
„Fehlendes Zertifikat holen“ fetches the verified issuer only on click through the hardened guard (proven live with letsencrypt.org) and marks it nachgeladen; the shared guard blocks every hidden IPv6 spelling of internal addresses with the old users' specs green; changelog item, CHANGELOG security bullet and guide paragraphs are in place; specs, tsc, biome and every live section are green on the rebuilt stack; one commit on main, not pushed.
Browser proof (per D-13, D-23). With Playwright MCP at http://localhost:3000 as admin/admin123. To keep this run small, take screenshots with a file name and request a page snapshot only when you need element references. Prepare in the session scratchpad a folder with copies of the fixtures: server.crt (rsa-leaf), intermediate.crt (rsa-inter), a vendor ZIP zertifikat-paket.zip (ec-leaf as ServerCertificate.crt, ec-inter, ec-root, ec-leaf-key.pem as server.key, a __MACOSX entry), rsa-compat.pfx, and letsencrypt-leaf.pem from openssl s_client. Switch to dark mode with the theme button and capture under .playwright-mcp/cert-manager/: select server.crt, then in a second selection intermediate.crt — both stay listed (the user's bug) — then the ZIP and the PFX, unlock the PFX with its password: ikt-dark-files.png; Analysieren: ikt-dark-analyze.png; Zusammenführen for the EC leaf with the root unticked, download the Fullchain and check the downloaded file holds two certificates (openssl on the saved file), open the PFX options: ikt-dark-merge.png; Konvertieren with the key to traditional with password: ikt-dark-convert.png; Vorlagen, download Nginx and Windows / IIS, snippet visible: ikt-dark-templates.png; „Alle entfernen“, add letsencrypt-leaf.pem, Zusammenführen shows „Zwischenzertifikat fehlt“ with the button: ikt-dark-gap.png; click it, the YE2 entry appears „nachgeladen von ye2.i.lencr.org“ (or the host the certificate names) and the next level shows its own calm note and button: ikt-dark-fetched.png; Dateien at 390×844: ikt-dark-mobile.png; reload the page and confirm the list is empty and the note visible. Then light mode: ikt-light-files.png, ikt-light-merge.png, ikt-light-templates.png, ikt-light-gap.png. Review every screenshot against D-23 (calm dense list, readable badges and warnings in both modes, visible focus, no ALL-CAPS labels, no arrow characters, no middle dots); fix findings in the module's web files with a test where the behaviour changes, rebuild web and re-shoot. Compare the labels quoted in the Anwenderanleitung with the page and correct the guide where they differ.
Todo and commit. git mv .planning/todos/pending/2026-10-09-cert-manager-mehrere-dateien-zip-fullchain.md to .planning/todos/completed/ if it is still pending. Commit feat(cert-manager): Browser-Nachweis und Abschluss with exactly the files of this task (the todo move plus any review fixes). Do not push, do not deploy. Append „## Task 8“ to the SUMMARY with output items 4 to 6.
pnpm --filter @tessera/api test && pnpm --filter @tessera/web test && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/cert-manager apps/api/src/common "apps/web/src/app/(portal)/modules/cert-manager" && pnpm exec biome check apps/api/src/cert-manager apps/api/src/main.ts apps/api/src/common/public-url-guard.ts apps/api/src/common/public-url-guard.spec.ts "apps/web/src/app/(portal)/modules/cert-manager/components" "apps/web/src/app/(portal)/modules/cert-manager/page.tsx" "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/working-set.test.ts" "apps/web/src/app/(portal)/modules/cert-manager/use-cert-workspace.ts" "apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx" apps/web/src/messages/de.json apps/web/src/messages/en.json && node .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/check-cert-messages.cjs 85 && test ! -e .planning/todos/pending/2026-10-09-cert-manager-mehrere-dateien-zip-fullchain.md && test -e .planning/todos/completed/2026-10-09-cert-manager-mehrere-dateien-zip-fullchain.md && docker compose ps --status running --services | grep -qx api && docker compose ps --status running --services | grep -qx web && docker compose logs api 2>&1 | grep -q "Cert-Manager module seeded in registry" && bash .planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh all && test "$(ls .playwright-mcp/cert-manager/ikt-dark-.png 2>/dev/null | wc -l)" -ge 8 && test "$(ls .playwright-mcp/cert-manager/ikt-light-.png 2>/dev/null | wc -l)" -ge 4 && echo "task8 ok"
<fails_when>non-zero exit of the chain and the last line task8 ok missing; the visible signal is one of: vitest printing FAIL or a failed count in either full suite, tsc printing error TS, biome printing Found with errors, check-cert-messages printing key mismatch, too few keys, title missing or bad text, the todo still under pending, the seed line absent from the api log, e2e-cert.sh printing E2E FAIL:, or fewer than eight dark or four light screenshots</fails_when>
After the user's own pull on alpha (the user deploys, not Claude): upload a real vendor ZIP in „Dateien“ and download the Fullchain; add a certificate in Nginx Proxy Manager with the „Nginx Proxy Manager“ template (research A2: field names and the RSA PRIVATE KEY header are assumptions to confirm there); import the „Windows / IIS“ PFX on a Windows server (compatible profile) and, if wanted, the „Modern“ PFX on a current Windows to see which systems accept it (research A1); check that „Fehlendes Zertifikat holen“ reaches the issuer from the alpha server (outbound http allowed).
Full api and web suites, both tsc, biome and every e2e section are green on the rebuilt stack; eight dark and four light screenshots prove the flow (two selections kept, ZIP and PFX, Fullchain without root, template, gap and fetch, empty after reload) and were reviewed; guide labels match the page; todo moved; one commit on main, not pushed.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
browser → API (/modules/cert-manager/analyze, build, fetch-issuer) |
untrusted caller with a valid session; files, ZIPs, PEM text, passwords, chosen formats and every PEM sent back are untrusted |
| uploaded containers → parsers | ZIP, ASN.1, PKCS#7, PKCS#12, keys and CSRs from unknown vendors or attackers; parsers run in the API process |
| API → internet (AIA caIssuers fetch) | outbound GET to an address named inside an uploaded certificate; answer untrusted |
| API → browser | analysis answers carry unencrypted private keys of the user's own upload (needed for stateless build) |
| repository fixtures | committed test-only private keys |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-ikt-01 | Denial of Service | ZIP expansion (zip bomb, many entries, nesting) | high | mitigate | D-17: magic-byte detection, ≤ 100 entries, ≤ 1 MiB declared per entry, ratio ≤ 100, total ≤ 20 MiB checked before any inflate, one level only, encrypted entries skipped; adm-zip bounds inflation to the declared size; specs with injected limits |
| T-ikt-02 | Denial of Service | multipart upload size and build body | medium | mitigate | 30 files × 5 MiB (multer), total ≤ 20 MiB → 413 tooLarge, web refuses over 10 MiB before upload; build has its own JSON limit of 512 KiB just above the DTO maximum (≈ 384 kB) with a coded 413 beyond (D-26), every other route keeps 100 kB; e2e checks 380 kB → 400 with code, over 600 KiB → 413 tooLarge, login with 150 kB → 413 |
| T-ikt-17 | Denial of Service | build body parser registration in main.ts |
medium | mitigate | the wrapper is named certBuildJsonBody, never jsonParser, because Nest would otherwise skip its global JSON parser for every route (D-26); the spec asserts the name, the JSON login in every e2e setup proves the global parser still runs |
| T-ikt-03 | Denial of Service | malformed ASN.1 / PEM / PKCS#12 | medium | mitigate | every detector in try/catch, bad blobs become ignored, never a 500; specs with random, truncated and broken input; PKCS#12 only for a SEQUENCE starting with INTEGER 3; at most 10 distinct passwords tried |
| T-ikt-04 | Tampering / SSRF | AIA fetch | high | mitigate | D-22: URL derived on the server from the uploaded certificate (DTO accepts only pem), http/https, default ports, no credentials, isPublicHttpUrl before the first request and every redirect (max 3), guarded connect lookup against DNS rebinding, 8 s, 256 KiB, no cookies or auth headers; specs per case; e2e proves 127.0.0.1 and extra url field are refused |
| T-ikt-05 | Tampering / SSRF | shared guard bypass via IPv6 spellings | high | mitigate | D-10 hardening with full IPv6 expansion (hex IPv4-mapped, IPv4-compatible, NAT64 incl. local-use, 6to4, Teredo, link/site-local, documentation, discard, zone ids) and a new spec; favorites and nextcloud-status specs re-run |
| T-ikt-06 | Spoofing | fetched certificate injected as issuer | medium | mitigate | only certificates with checkIssued AND verify against the incomplete certificate are returned; entry marked „nachgeladen von {host}“ |
| T-ikt-07 | Spoofing | wrong chain order or decoy CA from the browser | medium | mitigate | build re-runs buildChains on every call; a same-name CA with another key fails the signature check (spec with the decoy fixture); certificates outside the primary chain are dropped |
| T-ikt-08 | Information Disclosure | private keys and passwords | high | mitigate | D-11: nothing persisted, working set only in browser memory, passwords only in multipart/JSON bodies (never URLs), no logger call with bodies (request-log logs path and status only), errors carry codes only; e2e greps the api log for passwords, PRIVATE KEY and BEGIN CERTIFICATE |
| T-ikt-09 | Information Disclosure | AIA failure logging | low | mitigate | one warn line with host and code only, never PEM or URL path; spec asserts it |
| T-ikt-10 | Tampering | scoped forge patch in PFX writing | medium | mitigate | synchronous single call, originals restored in finally; spec asserts restoration after success and after an injected throw |
| T-ikt-11 | Elevation of Privilege | route access | medium | mitigate | class @UseModule('cert-manager') plus global JwtAuthGuard/TenantGuard as before; controller spec checks class metadata and the exact handler list; old routes removed (e2e: parse → 404) |
| T-ikt-12 | Tampering | file and friendly names from uploads | low | mitigate | names used for display only, never written to disk, control characters removed, max 255; download names and PFX friendlyName through safeBaseName |
| T-ikt-13 | Tampering (XSS) | subject strings, SANs, snippets rendered in the browser | low | mitigate | React text only, no dangerouslySetInnerHTML; snippet in a <pre> as text; clipboard gets plain text |
| T-ikt-14 | Denial of Service / Repudiation | AIA as a blind request trigger | low | accept | authenticated module users only, one GET per click to a public address on 80/443, answer only returned when it is a verified issuer certificate; noted in the header comment |
| T-ikt-15 | Information Disclosure | weak PFX encryption (3DES default) | low | accept | needed for older Windows servers (user decision D-07), „Modern (AES-256)“ selectable, guide explains the choice |
| T-ikt-16 | Information Disclosure | committed test private keys | low | accept | test-only PKI generated for this repo, CA keys never committed, README marks the folder for a future secret-scanner allow-list |
| T-ikt-SC | Tampering | npm/pip/cargo installs | low | accept | no new packages (research audit: all libraries already installed; adm-zip flagged SUS only for a recent release date and stays unchanged, no install) |
| </threat_model> |
| Source item | Covered by |
|---|---|
| GOAL / todo 1: merging accepts only one file, second overwrites first | Task 1 (append-only working set, FilesTab regression test), Task 8 (browser proof with two selections) |
| GOAL / todo 2: vendor ZIP unpacked and analysed | Task 3 (zip-expand, analyze, e2e), Task 4 (PFX and keys inside ZIPs) |
| GOAL / todo 3: choose output such as Fullchain, Tessera orders leaf, intermediates, root optional | Task 2 (chain + fullchain/chain), Task 5 (bundle, PFX, formats) |
| D-01 one upload tab, shared set, remove buttons, recognised content, other tabs without upload | Task 1 (FilesTab, page), Task 2 (EmptyWorkspace), Tasks 3–6 (remaining tabs on the same set) |
| D-02 root selectable, default without | Task 2 (MergeTab, build includeRoot), Task 5 (bundle/PFX), Task 6 (templates) |
| D-03 gap message + button-only SSRF-safe AIA fetch, marked nachgeladen | Task 2 (gap kinds and texts), Task 7 (cert-aia, ChainView button, marker, e2e live) |
| D-04 templates Nginx, Apache, IIS, NPM + HAProxy, Tomcat; no JKS; free choice stays | Task 6 (cert-templates, TemplatesTab), Task 5 (Konvertieren, Zusammenführen) |
| D-05 all formats in and out, RSA + EC, key match, chain via issuer + key ids | Task 1 (certificates PEM/DER), Task 2 (chain), Task 3 (PKCS#7), Task 4 (keys, PKCS#12, CSR, matching), Task 5 (outputs) |
| D-06 bug fixed structurally, analysis behaviour kept | Task 1 (working set), Task 3 (Analysieren from the old Übersicht), Task 4 (calm locked-PFX note) |
| D-07 PFX compat default / modern option, IIS compat | Task 5 (writePkcs12, PfxOptions, e2e algorithms), Task 6 (IIS/Tomcat templates) |
| D-08 old routes, service, specs, DTOs removed; export replaced by build; one parser | Task 1 (deletions, controller spec, grep gates), Tasks 2–7 (forge grep gate in every chain) |
| D-09 version 1.2.0 new entry 2026-10-09, CHANGELOG Unveröffentlicht | Task 6 (entry, bullets, version e2e), Task 7 (AIA item) |
| D-10 guard hardening with tests + CHANGELOG security line | Task 7 |
| D-11 keys/passwords never stored or logged, browser memory only, said in UI and guide | Tasks 1 and 4 (state, note, log greps), Task 5 (log grep), Task 6 (guide), Task 8 (reload proof) |
| D-12 docs Anwender, Betrieb, Entwicklung; de + en Sie texts | Tasks 6–7 (guides), Tasks 1–7 (messages, check-cert-messages gate) |
| D-13 fixtures, openssl round trips, browser proof | Task 1 (fixtures), Tasks 1–7 (e2e sections), Task 5 (openssl round trips), Task 8 (screenshots) |
| D-26 build body limit (checker info item: DTO caps vs. Express 100 kB) | Task 2 (cert-json-body + spec incl. maximal-body arithmetic, main.ts, e2e 380 kB / 600 KiB / login 150 kB), Task 6 (Betriebs- and Entwicklungsanleitung) |
| CONTEXT discretion: working-set location, ZIP limits, key-match display, CSR display, file names, server crypto, tab names | D-14, D-15, D-17, D-19, D-23, D-26 in Tasks 1–5 |
| CONTEXT specifics: chain via issuer/subject + AKI/SKI, gap messages, key check, keep analysis, version rule | D-18 (Tasks 2 and 4), D-09 (Task 6) |
| RESEARCH: forge cannot read EC certs/keys/CSR, PKCS#7 with EC fails | D-08, D-16 (node:crypto, ASN.1 walk), Tasks 1, 3, 4 |
| RESEARCH: bug root cause (shared single-file DropZone) | Task 1 (DropZone removed, page rebuilt) |
| RESEARCH: name-only chain walk wrong → checkIssued + verify, decoy and cross-signed | Task 2 (cert-chain spec) |
| RESEARCH: PFX EC bags (bag.cert null, bag.key false), OpenSSL-3 PFX readable, sniff by content | Task 4 (readPkcs12) |
| RESEARCH Pattern 3: scoped forge patch for EC PFX | D-20, Task 5 (spec incl. restoration) |
| RESEARCH: key lock detection and traditional encryption | Task 4 (cert-keys), Task 5 (exportKey) |
| RESEARCH: ZIP limits, magic bytes, junk, nested, encrypted | D-17, Task 3 |
| RESEARCH: AIA pattern from nextcloud-logo-fetch, server-derived URL, issuer verification, ports 80/443 (A8), guarded lookup | D-22, Task 7 |
| RESEARCH: shared guard IPv6 weaknesses | D-10, Task 7 |
| RESEARCH: templates table incl. NPM PKCS#1/SEC1 key (A2), HAProxy order (A3), Tomcat PKCS#12 (A4), Apache 2.4.8 split | D-21, Task 6, human check |
| RESEARCH: JSON 100 kB limit, NPM upload limit, outbound egress | D-26 (Task 2), Task 6 (Betriebsanleitung upload), Task 7 (Betriebsanleitung egress) |
| RESEARCH: per-file passwords pitfall | D-20, Task 4 |
| RESEARCH: biome array keys and nested buttons, umlaut guard, de/en parity, hard-coded German strings | Tasks 1–7 (messages via t(), check-cert-messages gate) |
| RESEARCH open questions 1–3 (AES option, old endpoints, version) | decided by orchestrator: D-07, D-08, D-09 |
| RESEARCH A1 (forge AES PFX on Windows), A2 (NPM fields) | human check after the user's pull (Task 8) |
| Deferred / out of scope: JKS (native tools), licensing and multi-tenancy topics | not planned |
<success_criteria>
- A user collects all files of a certificate delivery — several single files, a vendor ZIP, pasted text — in „Dateien“ without ever losing an earlier file, sees what each contains, and every other tab works on that list.
- Fullchain, Nur Kette, single certificate, certificate plus key, PFX (Kompatibel default, Modern optional), every key and CSR format and seven templates come out in the right order for RSA and EC, root only when ticked; openssl accepts every output in the e2e.
- A missing issuer is named clearly; „Fehlendes Zertifikat holen“ fetches it only on click through the hardened guard, accepts only the real issuer and marks it nachgeladen — proven live.
- No private key or password is stored or logged; the old routes, the old service and forge certificate parsing are gone; one parser remains.
- Module version 1.2.0 with module changelog, CHANGELOG (incl. the security fix), Anwender-, Betriebs- and Entwicklungsanleitung updated.
- A build request within the DTO caps is never cut off by the body limit; a larger one gets 413 with code tooLarge; every other route keeps 100 kB.
- After each of the eight tasks the gates of that task are green and the module is usable; at the end full api and web suites, both tsc runs, biome, all e2e sections green on the rebuilt stack; eight dark and four light screenshots reviewed; eight commits on main, nothing pushed, nothing deployed. </success_criteria>