---
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