179 lines
11 KiB
Markdown
179 lines
11 KiB
Markdown
---
|
||
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>
|