Files
tessera-ctl/.planning/phases/09-cert-manager-module/09-03-PLAN.md
T
schalli 063666af3b
Tessera CI/CD / Lint & Type Check (push) Failing after 41s
Tessera CI/CD / Tests (push) Has been skipped
Tessera CI/CD / Build & Publish Images (push) Has been skipped
docs(09): create cert-manager phase plan (6 plans)
2026-07-01 16:24:31 +02:00

185 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<artifacts>
## 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)
</artifacts>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: RED — failing parseCert spec</name>
<files>apps/api/src/cert-manager/cert-manager.service.spec.ts</files>
<read_first>
- 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)
</read_first>
<behavior>
- Test: parseCert({ pemText: <self-signed PEM> }) 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: <DER of the cert> } }) returns the same subject.cn (DER path uses 'binary' encoding).
- Test: parseCert({ file: { originalname:'c.pfx', buffer: <PFX built with password 'secret'> }, password: 'secret' }) returns CertDetails for the enclosed cert.
- Test: parseCert({ file: { originalname:'c.pfx', buffer: <same PFX> }, password: 'wrong' }) throws BadRequestException (asserted via rejects.toThrow / expect(() => ...).toThrow with the Nest exception).
- Test: parseCert({ pemText: 'not a cert' }) throws BadRequestException.
</behavior>
<action>
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.
</action>
<verify>
<automated>pnpm --filter @tessera/api test cert-manager --run 2>&1 | grep -Eiq 'fail|NotImplemented|✗|×' && echo RED_CONFIRMED</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>Failing parseCert spec committed (RED) covering all input formats + wrong-password + malformed cases.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: GREEN — implement parseCert + wire POST /parse</name>
<files>apps/api/src/cert-manager/cert-manager.service.ts, apps/api/src/cert-manager/cert-manager.controller.ts</files>
<read_first>
- 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)
</read_first>
<action>
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).
</action>
<verify>
<automated>pnpm --filter @tessera/api test cert-manager --run</automated>
</verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<done>parseCert returns full CertDetails for PEM/DER/PFX/P7B, throws 400 on wrong password/malformed input, and POST /parse is wired; API tests green.</done>
</task>
<task type="auto">
<name>Task 3: Inspect tab UI + action + render test</name>
<files>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</files>
<read_first>
- 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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm --filter @tessera/web test cert-manager --run</automated>
</verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<done>Inspect tab loads a cert end-to-end, renders the details grid on success and a localized destructive error on failure; web tests green.</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
Create `.planning/phases/09-cert-manager-module/09-03-SUMMARY.md` when done
</output>