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.
Task 1: RED — failing convertCert spec
apps/api/src/cert-manager/cert-manager.service.spec.ts
- 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)
- Test: convertCert({ pemText: , 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: }, targetFormat: 'pem' }) returns a PEM whose parsed cert subject.cn equals the original (DER->PEM round trip identical).
- Test: convertCert({ pemText: , targetFormat: 'p7b' }) returns a P7B whose enclosed cert count is 1.
- Test: convertCert({ pemText: 'garbage', targetFormat: 'der' }) throws BadRequestException.
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.
pnpm --filter @tessera/api test cert-manager --run 2>&1 | grep -Eiq 'fail|NotImplemented|✗|×' && echo RED_CONFIRMED
- convertCert tests exist for PEM->DER, DER->PEM (identity), PEM->P7B, and malformed input
- Suite shows convertCert tests failing while all prior tests pass
Failing convertCert spec committed (RED) including a round-trip identity assertion.
Task 2: GREEN — implement convertCert + wire POST /convert
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.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)
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.
pnpm --filter @tessera/api test cert-manager --run
- `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
convertCert converts between PEM/DER/P7B with byte-identical round trips and 400 on malformed input; POST /convert wired; API tests green.
Task 3: Convert tab UI + action + render test
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
- 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')
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.
pnpm --filter @tessera/web test cert-manager --run
- `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
Convert tab converts and downloads end-to-end for PEM/DER/P7B; web tests green.
<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>
- `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
<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>
Create `.planning/phases/09-cert-manager-module/09-05-SUMMARY.md` when done