176 lines
10 KiB
Markdown
176 lines
10 KiB
Markdown
---
|
||
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>
|