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

179 lines
11 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: 05
type: execute
wave: 4
depends_on: [09-04]
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/ConvertTab.tsx
- apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
autonomous: true
requirements: [CERT-04]
must_haves:
truths:
- "A user uploads a certificate in any supported format and downloads it converted to a chosen target format (PEM, DER, P7B)"
- "A PEM -> DER -> PEM round trip yields a byte-identical certificate"
- "The Convert tab offers a target-format selector and a download button for the converted file"
artifacts:
- "CertManagerService.convertCert implemented (any input -> PEM/DER/P7B target)"
- "POST /modules/cert-manager/convert wired to convertCert"
- "ConvertTab.tsx renders format selector + download"
key_links:
- "ConvertTab -> convertCertAction -> POST /modules/cert-manager/convert -> CertManagerService.convertCert"
- "convertCert returns { filename, content(base64), mimeType } -> downloadBase64"
---
<objective>
Vertical slice: convert a certificate between formats. Implement CertManagerService.convertCert (parse any supported input, re-serialize to the chosen target), wire POST /convert, and build the Convert tab with a target-format selector and download.
MVP: after this plan a user can upload a cert and download it in a different format — a complete capability (CERT-04). (PFX output as a convert target is delivered together with the PFX-create logic in Plan 06; this plan covers PEM/DER/P7B targets.)
Purpose: Delivers CERT-04 for PEM/DER/P7B round-trips, reusing parse helpers + base64-download.
Output: Working Convert tab end-to-end + tested convertCert 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-04-SUMMARY.md
</context>
<artifacts>
## Artifacts this plan produces
- Implemented method: `CertManagerService.convertCert({ file?, pemText?, targetFormat, password? }): FileResponse`
- New TS interface: `FileResponse` ({ filename, content(base64), mimeType }) per RESEARCH Convert/Merge Response Shape
- Target-format -> mimeType map (pem: application/x-pem-file, der: application/x-x509-ca-cert, p7b: application/x-pkcs7-certificates)
- Wired route: `POST /modules/cert-manager/convert` (FileInterceptor('file') + @Body targetFormat/password)
- New action: `convertCertAction(input, targetFormat)` in actions.ts
- Implemented component: `ConvertTab` (format selector + convert button + download)
</artifacts>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: RED — failing convertCert 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 (fixtures + prior tests)
- apps/api/src/cert-manager/cert-manager.service.ts (convertCert stub + parse helpers reused from parseCert)
- .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 certificateToPem / certificateToAsn1 -> toDer; Convert Response Shape; Pitfall 1 binary encoding)
</read_first>
<behavior>
- Test: convertCert({ pemText: <self-signed PEM>, targetFormat: 'der' }) returns FileResponse with mimeType application/x-x509-ca-cert and content that base64-decodes to DER bytes which, re-parsed, equal the original cert (round-trip).
- Test: convertCert({ file: { originalname:'c.der', buffer: <DER> }, targetFormat: 'pem' }) returns a PEM whose parsed cert subject.cn equals the original (DER->PEM round trip identical).
- Test: convertCert({ pemText: <PEM>, targetFormat: 'p7b' }) returns a P7B whose enclosed cert count is 1.
- Test: convertCert({ pemText: 'garbage', targetFormat: 'der' }) throws BadRequestException.
</behavior>
<action>
Add the Behavior tests to cert-manager.service.spec.ts, building DER fixtures from the self-signed cert. Assert round-trip identity by re-parsing the converted output and comparing the DER bytes (or subject + fingerprint). Confirm RED against the convertCert stub. Do not implement convertCert 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>
- convertCert tests exist for PEM->DER, DER->PEM (identity), PEM->P7B, and malformed input
- Suite shows convertCert tests failing while all prior tests pass
</acceptance_criteria>
<done>Failing convertCert spec committed (RED) including a round-trip identity assertion.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: GREEN — implement convertCert + wire POST /convert</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 (parse helpers, detectFormat, toForgeBuffer)
- apps/api/src/cert-manager/cert-manager.controller.ts (convert route stub)
- apps/api/src/cert-manager/cert-manager.service.spec.ts (RED contract)
- .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 serialization: certificateToPem, certificateToAsn1 -> asn1.toDer -> bytesToHex -> Buffer; pkcs7 create for P7B)
</read_first>
<action>
Implement CertManagerService.convertCert: parse the input to a forge cert reusing the same input-resolution logic as parseCert (extract a shared private helper if helpful). Serialize to targetFormat: 'pem' via certificateToPem; 'der' via asn1.toDer(certificateToAsn1(cert)).getBytes() -> Buffer.from(bytesToHex, 'hex'); 'p7b' via forge.pkcs7.createSignedData / addCertificate then messageToPem (or asn1 -> DER). Build FileResponse: filename `converted.${targetFormat}`, content = base64 of the output bytes (for PEM/P7B text use Buffer.from(str,'utf-8').toString('base64'); for DER use derBuffer.toString('base64')), mimeType from the target->mime map. Reject an unsupported targetFormat and wrap all forge calls in try/catch -> BadRequestException. Never log password. In the controller, POST convert uses FileInterceptor('file', { limits: { fileSize: 5*1024*1024 } }), reads @Body('targetFormat') and @Body('password'), rejects when neither file nor pemText present. Run 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 convertCert tests passing including the round-trip identity test
- `grep -q "converted." apps/api/src/cert-manager/cert-manager.service.ts` (filename built) and DER path uses toString('base64') on a Buffer, not 'utf-8'
- `pnpm --filter @tessera/api type-check` exits 0
</acceptance_criteria>
<done>convertCert converts between PEM/DER/P7B with byte-identical round trips and 400 on malformed input; POST /convert wired; API tests green.</done>
</task>
<task type="auto">
<name>Task 3: Convert 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/ConvertTab.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/ConvertTab.tsx (empty-state stub)
- apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx (result/loading/error pattern)
- apps/web/src/app/(portal)/modules/cert-manager/actions.ts (postForm, downloadBase64)
- apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx (wiring)
- .planning/phases/09-cert-manager-module/09-UI-SPEC.md (Konvertieren = format selector + single download button; empty state 'Keine Datei geladen.' + 'waehle ein Ausgabeformat')
</read_first>
<action>
Add convertCertAction(input, targetFormat) to actions.ts: build FormData with file (or send JSON with pemText) plus targetFormat and optional password; call postForm('convert', form); return FileResponse.
Implement ConvertTab: accept { file, pemText, password }. Render a target-format selector (native select) offering pem, der, p7b (labels localized; PFX intentionally not offered here — added in Plan 06). Primary 'Konvertieren' button (t('actions.convert'), loading label swap). On success call downloadBase64(filename, content, mimeType) from the FileResponse. Empty state t('emptyState.convert'); localized error in text-destructive on failure.
Extend cert-manager.test.tsx: assert the format selector renders pem/der/p7b options; mock convertCertAction to resolve a FileResponse and assert downloadBase64 is invoked after clicking Konvertieren.
</action>
<verify>
<automated>pnpm --filter @tessera/web test cert-manager --run</automated>
</verify>
<acceptance_criteria>
- `grep -q "convertCertAction" apps/web/src/app/(portal)/modules/cert-manager/actions.ts`
- ConvertTab format selector renders pem, der, p7b options
- `pnpm --filter @tessera/web test cert-manager --run` exits 0 including the new ConvertTab tests
- `pnpm --filter @tessera/web type-check` exits 0
</acceptance_criteria>
<done>Convert tab converts and downloads end-to-end for PEM/DER/P7B; web tests green.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| client -> API /convert | Untrusted cert bytes enter node-forge parse + re-serialize |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-01 | Tampering | convertCert (node-forge) | medium | mitigate | try/catch around parse + serialize -> BadRequestException; unsupported targetFormat rejected as 400 |
| T-09-06 | Tampering | binary encoding on DER output | medium | mitigate | DER built via bytesToHex -> Buffer.from(hex) -> base64; never utf-8 round-trip (Pitfall 1) |
| T-09-03 | Denial of Service | POST /convert upload | high | mitigate | FileInterceptor `limits.fileSize` = 5 MB |
| T-09-04 | Elevation of Privilege | POST /convert | high | mitigate | Global JwtAuthGuard + `@UseModule('cert-manager')` |
</threat_model>
<verification>
- `pnpm --filter @tessera/api test cert-manager --run` — convertCert green incl. round-trip identity
- `pnpm --filter @tessera/web test cert-manager --run` — ConvertTab green
- type-checks clean
- Manual (phase gate): convert a real PEM to DER, re-upload the DER to Inspect, confirm identical cert
</verification>
<success_criteria>
- convertCert converts PEM/DER/P7B with byte-identical round trips (CERT-04)
- Convert tab works end-to-end
- All tests green; type-checks clean
</success_criteria>
<output>
Create `.planning/phases/09-cert-manager-module/09-05-SUMMARY.md` when done
</output>