--- phase: 09-cert-manager-module plan: 03 type: execute wave: 2 depends_on: [09-01, 09-02] files_modified: - apps/api/src/cert-manager/cert-manager.service.ts - apps/api/src/cert-manager/cert-manager.controller.ts - apps/api/src/cert-manager/cert-manager.service.spec.ts - apps/web/src/app/(portal)/modules/cert-manager/actions.ts - apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx - apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx autonomous: true requirements: [CERT-01, CERT-05] must_haves: truths: - "A user uploads (or pastes) a PEM/DER/PFX/P7B certificate and sees subject, issuer, validity, SANs, key type/size, serial, signature algorithm, and SHA-1 + SHA-256 fingerprints" - "A password-protected PFX is parsed when the correct password is supplied; a wrong password returns HTTP 400 (not 500)" - "The Inspect tab renders a key-value result grid on success and a localized error on failure" artifacts: - "CertManagerService.parseCert implemented (PEM/DER/PFX/P7B -> CertDetails)" - "POST /modules/cert-manager/parse wired to parseCert" - "InspectTab.tsx renders the CertDetails grid + inspect action" key_links: - "InspectTab -> inspectCertAction -> POST /modules/cert-manager/parse -> CertManagerService.parseCert" - "parseCert wraps node-forge in try/catch -> BadRequestException (wrong password / malformed)" --- First functional vertical slice: certificate inspection. Implement CertManagerService.parseCert to accept an uploaded file (PEM/DER/PFX/P7B) or pasted PEM text plus an optional PFX password, and return structured CertDetails. Wire the POST /parse endpoint and build the Inspect tab to render the result grid. MVP: after this plan a user can activate the module, upload a cert, and read its parsed details — a complete end-to-end capability (CERT-01). Password-protected PFX open (CERT-05 read half) is covered because parsing a .pfx requires the supplied password. Purpose: Delivers CERT-01 and the read half of CERT-05; establishes the parse-and-render pattern reused by later slices. Output: Working Inspect tab end-to-end + tested parseCert service. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/09-cert-manager-module/09-CONTEXT.md @.planning/phases/09-cert-manager-module/09-RESEARCH.md @.planning/phases/09-cert-manager-module/09-PATTERNS.md @.planning/phases/09-cert-manager-module/09-UI-SPEC.md @.planning/phases/09-cert-manager-module/09-01-SUMMARY.md @.planning/phases/09-cert-manager-module/09-02-SUMMARY.md ## Artifacts this plan produces - Implemented method: `CertManagerService.parseCert({ file?, pemText?, password? }): CertDetails` - New TS interface: `CertDetails` (subject, issuer, validity{notBefore,notAfter,isExpired,daysLeft}, san[], keyType, keyBits, serialNumber, signatureAlgorithm, fingerprint{sha1,sha256}, pemPreview) — per RESEARCH Inspect Response Shape - Wired route: `POST /modules/cert-manager/parse` (FileInterceptor('file') + @Body pemText/password) - New action: `inspectCertAction(input)` in actions.ts (JSON path for pemText, multipart path for file) - Implemented component: `InspectTab` (key-value result grid + inspect button + loading/error) Task 1: RED — failing parseCert spec apps/api/src/cert-manager/cert-manager.service.spec.ts - apps/api/src/cert-manager/cert-manager.service.spec.ts (existing helper/seed tests + beforeAll self-signed cert generator from Plan 01) - apps/api/src/cert-manager/cert-manager.service.ts (current parseCert stub throwing NotImplementedException + helpers) - .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 node-forge parse APIs; Inspect Response Shape; Pitfall 1 binary encoding) - Test: parseCert({ pemText: }) returns CertDetails with subject.cn matching the generated CN, fingerprint.sha256 matching /^[0-9A-F]{2}(:[0-9A-F]{2})+$/, keyType 'RSA', keyBits 2048, and validity.isExpired false. - Test: parseCert({ file: { originalname:'c.der', buffer: } }) returns the same subject.cn (DER path uses 'binary' encoding). - Test: parseCert({ file: { originalname:'c.pfx', buffer: }, password: 'secret' }) returns CertDetails for the enclosed cert. - Test: parseCert({ file: { originalname:'c.pfx', buffer: }, password: 'wrong' }) throws BadRequestException (asserted via rejects.toThrow / expect(() => ...).toThrow with the Nest exception). - Test: parseCert({ pemText: 'not a cert' }) throws BadRequestException. Extend cert-manager.service.spec.ts with the Behavior tests above. Build the DER and password-protected PFX fixtures in the spec from the beforeAll self-signed cert using node-forge (forge.asn1.toDer + forge.pkcs12.toPkcs12Asn1 with password 'secret'). Run the suite and confirm these new tests FAIL against the current parseCert stub (RED). Do not implement parseCert in this task. pnpm --filter @tessera/api test cert-manager --run 2>&1 | grep -Eiq 'fail|NotImplemented|✗|×' && echo RED_CONFIRMED - New parseCert tests exist in cert-manager.service.spec.ts covering PEM, DER, PFX-correct-password, PFX-wrong-password (BadRequestException), and malformed input - Running the suite shows the parseCert tests failing (RED) while the Plan 01 helper/seed tests still pass Failing parseCert spec committed (RED) covering all input formats + wrong-password + malformed cases. Task 2: GREEN — implement parseCert + wire POST /parse apps/api/src/cert-manager/cert-manager.service.ts, apps/api/src/cert-manager/cert-manager.controller.ts - apps/api/src/cert-manager/cert-manager.service.ts (helpers detectFormat/toForgeBuffer/getFingerprint/parsePemChain from Plan 01) - apps/api/src/cert-manager/cert-manager.controller.ts (parse route stub + FileInterceptor from Plan 01) - apps/api/src/cert-manager/cert-manager.service.spec.ts (the RED tests from Task 1 — target contract) - .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 full node-forge API: certificateFromPem, fromDer, pkcs12FromAsn1, subject/issuer getField, SAN extraction, RSA bitLength; Inspect Response Shape) Implement CertManagerService.parseCert to: resolve input (pemText -> parse as PEM/chain; file -> detectFormat, then PEM via certificateFromPem, DER via asn1.fromDer(toForgeBuffer(...)) + certificateFromAsn1, PFX via pkcs12FromAsn1(asn1, password ?? '') then extract certBag, P7B via messageFromPem or messageFromAsn1 depending on content sniff). Build CertDetails: subject/issuer CN/O/OU/C via cert.subject.getField / cert.issuer.getField; validity.notBefore/notAfter from cert.validity, isExpired and daysLeft computed against now; san[] from the subjectAltName extension; keyType 'RSA'/'EC' and keyBits from the public key bitLength; serialNumber; signatureAlgorithm from the cert; fingerprint.sha1 and .sha256 via getFingerprint; pemPreview via certificateToPem. Wrap ALL node-forge calls in try/catch and throw BadRequestException with a generic message on failure (covers malformed cert AND wrong PFX password -> 400, threat T-09-01/T-09-02). Never log the password. Define and export the CertDetails interface (co-located in the service or a types file). In cert-manager.controller.ts, ensure POST parse uses FileInterceptor('file', { limits: { fileSize: 5*1024*1024 } }), reads @Body('pemText') and @Body('password'), rejects when neither file nor pemText present (BadRequestException), and delegates to parseCert. Run the suite until all parseCert tests pass (GREEN). pnpm --filter @tessera/api test cert-manager --run - `pnpm --filter @tessera/api test cert-manager --run` exits 0 with all parseCert tests passing - `grep -q "BadRequestException" apps/api/src/cert-manager/cert-manager.service.ts` in the parseCert catch path - No `console.log`/logger call in the service references the password value (grep shows no `password` argument passed to logger) - `grep -q "fileSize: 5" apps/api/src/cert-manager/cert-manager.controller.ts` - `pnpm --filter @tessera/api type-check` exits 0 parseCert returns full CertDetails for PEM/DER/PFX/P7B, throws 400 on wrong password/malformed input, and POST /parse is wired; API tests green. Task 3: Inspect tab UI + action + render test apps/web/src/app/(portal)/modules/cert-manager/actions.ts, apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx, apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx - apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx (empty-state stub from Plan 02) - apps/web/src/app/(portal)/modules/cert-manager/actions.ts (API_URL, postForm, downloadBase64 from Plan 02) - apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx (shell tests + i18n wiring from Plan 02) - apps/web/src/app/(portal)/modules/domaincheck/page.tsx (loading/error state pattern) - .planning/phases/09-cert-manager-module/09-UI-SPEC.md (Analysieren result = key-value grid grid-cols-2 gap-2 text-sm; loading label swap; error text-destructive) Add inspectCertAction to actions.ts: if pemText is present, POST JSON { pemText, password } to /modules/cert-manager/parse with Content-Type application/json; else build FormData with file + optional password and use the postForm('parse', form) helper. Return the parsed CertDetails JSON; throw on !ok (reuse postForm error behavior for the multipart path). Implement InspectTab: accept { file, pemText, password }. Render a primary 'Analysieren' button (t('actions.inspect'), disabled + label t('actions.processing') while loading, per UI-SPEC). On click call inspectCertAction and store the result; on error store a localized message (wrong-password -> t('error.wrongPassword'), unknown format -> t('error.unknownFormat'), else t('error.generic')). Render the empty state (t('emptyState.inspect')) when no result; render a grid grid-cols-2 gap-2 text-sm of subject/issuer/validity/SANs/keyType/keyBits/serial/signatureAlgorithm/fingerprint.sha1/fingerprint.sha256 on success; render error in text-sm text-destructive. All labels via t(). No shadcn. Extend cert-manager.test.tsx with a test that mocks inspectCertAction to resolve a CertDetails object and asserts the InspectTab renders the subject CN and the sha256 fingerprint after clicking Analysieren; and a test that mocks a rejection and asserts a text-destructive error is shown. pnpm --filter @tessera/web test cert-manager --run - `grep -q "inspectCertAction" apps/web/src/app/(portal)/modules/cert-manager/actions.ts` - InspectTab shows the empty state before a result and a key-value grid (grid-cols-2) after a successful inspect - `pnpm --filter @tessera/web test cert-manager --run` exits 0 including the new success + error InspectTab tests - `pnpm --filter @tessera/web type-check` exits 0 Inspect tab loads a cert end-to-end, renders the details grid on success and a localized destructive error on failure; web tests green. ## Trust Boundaries | Boundary | Description | |----------|-------------| | client -> API /parse | Untrusted cert bytes + optional PFX password enter node-forge parsing | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-09-01 | Tampering | CertManagerService.parseCert (node-forge) | medium | mitigate | Every node-forge call wrapped in try/catch; malformed cert -> BadRequestException (400), never an unhandled 500 | | T-09-02 | Information Disclosure | parseCert password handling | high | mitigate | Wrong PFX password caught -> generic 400 message; password value never logged and never echoed in the response | | T-09-03 | Denial of Service | POST /parse upload | high | mitigate | FileInterceptor `limits.fileSize` = 5 MB caps in-memory buffer | | T-09-04 | Elevation of Privilege | POST /parse | high | mitigate | Global JwtAuthGuard + `@UseModule('cert-manager')` on the controller | - `pnpm --filter @tessera/api test cert-manager --run` — parseCert suite green (all formats + wrong password 400) - `pnpm --filter @tessera/web test cert-manager --run` — InspectTab success + error tests green - `pnpm --filter @tessera/api type-check` and `pnpm --filter @tessera/web type-check` clean - Manual (phase gate): upload a real cert, verify grid; upload a password PFX with wrong then right password - parseCert returns full CertDetails for PEM/DER/PFX/P7B (CERT-01) and opens password PFX with correct password / 400 on wrong (CERT-05 read) - Inspect tab works end-to-end with localized errors - All API + web tests green; type-checks clean Create `.planning/phases/09-cert-manager-module/09-03-SUMMARY.md` when done