Files
tessera-ctl/.planning/phases/09-cert-manager-module/09-04-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

176 lines
10 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: 04
type: execute
wave: 3
depends_on: [09-03]
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/SplitTab.tsx
- apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
autonomous: true
requirements: [CERT-02]
must_haves:
truths:
- "A user uploads a fullchain.pem or a P7B bundle and receives each individual certificate as a separately downloadable file"
- "The Split tab lists one download button per returned certificate with its subject CN and expiry"
artifacts:
- "CertManagerService.splitCerts implemented (fullchain PEM + P7B -> array of certs)"
- "POST /modules/cert-manager/split wired to splitCerts"
- "SplitTab.tsx renders per-cert download list"
key_links:
- "SplitTab -> splitCertsAction -> POST /modules/cert-manager/split -> CertManagerService.splitCerts"
- "each returned cert.content (base64 PEM) -> downloadBase64 on click"
---
<objective>
Vertical slice: split a fullchain.pem or a P7B/PKCS7 bundle into its individual certificates, each downloadable. Implement CertManagerService.splitCerts, wire POST /split, and build the Split tab to list per-cert download buttons.
MVP: after this plan a user can upload a chain/bundle and download each cert individually — a complete capability (CERT-02).
Purpose: Delivers CERT-02, reusing the parse helpers and the base64-download pattern.
Output: Working Split tab end-to-end + tested splitCerts 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-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-03-SUMMARY.md
</context>
<artifacts>
## Artifacts this plan produces
- Implemented method: `CertManagerService.splitCerts({ file }): SplitResponse`
- New TS interface: `SplitResponse` ({ count, certs: [{ index, filename, content(base64 PEM), subject{cn}, validity{notAfter} }] }) per RESEARCH Split Response Shape
- Wired route: `POST /modules/cert-manager/split` (FileInterceptor('file'))
- New action: `splitCertsAction(file)` in actions.ts
- Implemented component: `SplitTab` (per-cert download list)
</artifacts>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: RED — failing splitCerts 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 (self-signed cert generator + fixtures from prior plans)
- apps/api/src/cert-manager/cert-manager.service.ts (splitCerts stub + parsePemChain helper)
- .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 parsePemChain + pkcs7 messageFromPem/messageFromAsn1; Split Response Shape; Pitfall 4 P7B binary vs PEM)
</read_first>
<behavior>
- Test: splitCerts({ file: { originalname:'fullchain.pem', buffer: <two concatenated cert PEMs> } }) returns count 2 and certs[0]/certs[1] each with a base64 content that decodes to a single valid PEM (contains one BEGIN CERTIFICATE block) and a subject.cn.
- Test: splitCerts on a P7B PEM bundle (built via forge.pkcs7 from the test certs) returns the enclosed certs count.
- Test: splitCerts({ file: { originalname:'x.pem', buffer: <garbage> } }) throws BadRequestException.
</behavior>
<action>
Add the Behavior tests to cert-manager.service.spec.ts. Build the fullchain fixture by concatenating two self-signed cert PEMs; build the P7B fixture with node-forge pkcs7. Confirm the tests FAIL against the splitCerts stub (RED). Do not implement splitCerts here.
</action>
<verify>
<automated>pnpm --filter @tessera/api test cert-manager --run 2>&1 | grep -Eiq 'fail|NotImplemented|✗|×' && echo RED_CONFIRMED</automated>
</verify>
<acceptance_criteria>
- splitCerts tests exist covering fullchain PEM, P7B bundle, and malformed input
- The suite shows splitCerts tests failing while all prior tests still pass
</acceptance_criteria>
<done>Failing splitCerts spec committed (RED).</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: GREEN — implement splitCerts + wire POST /split</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 (parsePemChain, detectFormat, toForgeBuffer helpers)
- apps/api/src/cert-manager/cert-manager.controller.ts (split route stub + FileInterceptor)
- apps/api/src/cert-manager/cert-manager.service.spec.ts (RED contract from Task 1)
- .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 PEM chain split + pkcs7 parse; Pitfall 4 sniff -----BEGIN for PEM vs DER P7B)
</read_first>
<action>
Implement CertManagerService.splitCerts({ file }): detectFormat; for PEM/CRT use parsePemChain to get the cert array; for P7B sniff the first bytes — if the buffer contains '-----BEGIN' use forge.pkcs7.messageFromPem, else asn1.fromDer(toForgeBuffer(...)) + messageFromAsn1 — and read the .certificates array. Build SplitResponse: count plus certs[] where each entry has index, filename `cert-${index+1}.pem`, content = base64 of certificateToPem(cert), subject.cn and validity.notAfter. Wrap in try/catch -> BadRequestException. In the controller, POST split uses FileInterceptor('file', { limits: { fileSize: 5*1024*1024 } }), rejects a missing file, and delegates. Run the suite to 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 splitCerts tests passing
- Each returned cert content base64-decodes to exactly one BEGIN CERTIFICATE block
- `grep -q "messageFromPem" apps/api/src/cert-manager/cert-manager.service.ts` (P7B path present)
- `pnpm --filter @tessera/api type-check` exits 0
</acceptance_criteria>
<done>splitCerts returns individual base64 PEM certs for fullchain + P7B, 400 on malformed; POST /split wired; API tests green.</done>
</task>
<task type="auto">
<name>Task 3: Split 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/SplitTab.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/SplitTab.tsx (empty-state stub)
- apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx (established loading/error/result pattern from Plan 03)
- apps/web/src/app/(portal)/modules/cert-manager/actions.ts (postForm, downloadBase64)
- apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx (test wiring)
- .planning/phases/09-cert-manager-module/09-UI-SPEC.md (Aufteilen result = list of certs, each with a bg-secondary download button; empty state 'Keine Datei geladen.')
</read_first>
<action>
Add splitCertsAction(file) to actions.ts: build FormData with the file and call postForm('split', form); return the SplitResponse.
Implement SplitTab: accept { file }. Primary 'Aufteilen' button (t('actions.split'), disabled + t('actions.processing') while loading). On success store certs[] and render a list — each row shows the cert subject.cn + validity.notAfter and a secondary download button (bg-secondary text-secondary-foreground, t('actions.download')) that calls downloadBase64(filename, content, 'application/x-pem-file'). Empty state t('emptyState.split') when no result; localized error (t('error.generic')/t('error.unknownFormat')) in text-destructive on failure.
Extend cert-manager.test.tsx: mock splitCertsAction to resolve two certs and assert SplitTab renders two download buttons after clicking Aufteilen.
</action>
<verify>
<automated>pnpm --filter @tessera/web test cert-manager --run</automated>
</verify>
<acceptance_criteria>
- `grep -q "splitCertsAction" apps/web/src/app/(portal)/modules/cert-manager/actions.ts`
- SplitTab renders one download button per returned cert; clicking it calls downloadBase64
- `pnpm --filter @tessera/web test cert-manager --run` exits 0 including the new SplitTab test
- `pnpm --filter @tessera/web type-check` exits 0
</acceptance_criteria>
<done>Split tab uploads a chain/bundle and lists downloadable per-cert files end-to-end; web tests green.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| client -> API /split | Untrusted chain/bundle bytes enter node-forge parsing |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-01 | Tampering | splitCerts (node-forge PEM/PKCS7) | medium | mitigate | try/catch around parsePemChain + pkcs7 parse -> BadRequestException on malformed bundle |
| T-09-03 | Denial of Service | POST /split upload | high | mitigate | FileInterceptor `limits.fileSize` = 5 MB |
| T-09-04 | Elevation of Privilege | POST /split | high | mitigate | Global JwtAuthGuard + `@UseModule('cert-manager')` |
</threat_model>
<verification>
- `pnpm --filter @tessera/api test cert-manager --run` — splitCerts green
- `pnpm --filter @tessera/web test cert-manager --run` — SplitTab green
- type-checks clean
- Manual (phase gate): upload a real fullchain.pem, download each cert, verify each opens as a valid single cert
</verification>
<success_criteria>
- splitCerts splits fullchain PEM + P7B into individual downloadable certs (CERT-02)
- Split tab works end-to-end
- All tests green; type-checks clean
</success_criteria>
<output>
Create `.planning/phases/09-cert-manager-module/09-04-SUMMARY.md` when done
</output>