Files
tessera-ctl/.planning/phases/09-cert-manager-module/09-05-SUMMARY.md
T

179 lines
9.8 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"
subsystem: api+frontend
tags: [cert-manager, convertCert, node-forge, p7b, convert-tab, tdd, vitest, file-response]
dependency_graph:
requires: [09-01, 09-02, 09-03, 09-04]
provides: [cert-manager-convert-endpoint, file-response-interface, convert-tab-ui]
affects:
- 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
tech_stack:
added: []
patterns: [tdd-red-green, node-forge-der-hex-base64, file-response-download, vi.spyOn-downloadBase64]
key_files:
created: []
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
decisions:
- "DER output: bytesToHex → Buffer.from(hex,'hex') → base64 to avoid utf-8 corruption (Pitfall 1, T-09-06)"
- "P7B output: pkcs7.createSignedData + asn1.toDer + pem.encode (PEM-wrapped PKCS7)"
- "FORMAT_MIME map at module level for pem/der/p7b mimeType lookup"
- "Controller convert: adds @Body('pemText') so text-paste path works; rejects when neither file nor pemText"
- "ConvertTab shows empty state + format selector simultaneously (mirrors InspectTab button-always-visible pattern)"
metrics:
duration: "~8 minutes"
completed: "2026-07-02T07:39:00Z"
tasks_completed: 3
files_created: 0
files_modified: 6
requirements: [CERT-04]
status: complete
---
# Phase 09 Plan 05: Convert Vertical Slice Summary
**One-liner:** convertCert converts PEM/DER/P7B with byte-identical round trips using bytesToHex→hex→base64 for DER safety; POST /convert wired; ConvertTab renders format selector + Konvertieren button + blob download.
## Objective
Third functional vertical slice: certificate format conversion. Delivers CERT-04 (PEM/DER/P7B targets). Reuses parse helpers from Plans 03–04 and downloadBase64 pattern from Plan 04.
## Tasks Completed
| # | Name | Type | Commit | Status |
|---|------|------|--------|--------|
| 1 | RED — failing convertCert spec | test | 37db58b | done |
| 2 | GREEN — implement convertCert + wire POST /convert | feat | 5969464 | done |
| 3 | Convert tab UI + convertCertAction + render test | feat | f89d656 | done |
## What Was Built
### Task 1: RED — failing convertCert spec
Added 4 new convertCert tests to `cert-manager.service.spec.ts`:
- **PEM→DER round-trip:** calls `convertCert({ pemText, targetFormat: 'der' })`, decodes base64 DER, re-parses via `forge.asn1.fromDer` + `forge.pki.certificateFromAsn1`, asserts CN matches original
- **DER→PEM round-trip:** calls `convertCert({ file: { originalname: 'c.der', buffer }, targetFormat: 'pem' })`, decodes base64 PEM, re-parses via `forge.pki.certificateFromPem`, asserts CN matches
- **PEM→P7B:** calls `convertCert({ pemText, targetFormat: 'p7b' })`, decodes base64 PKCS7 PEM, parses via `forge.pkcs7.messageFromPem`, asserts ≥1 certificate enclosed
- **Malformed input:** expects BadRequestException for garbage PEM text
All 4 fail against NotImplementedException stub (RED confirmed). 19 prior tests remain green.
### Task 2: GREEN — convertCert implementation
**`cert-manager.service.ts`:**
- Exported `FileResponse` interface: `{ filename, content (base64), mimeType }`
- Added `FORMAT_MIME` map: `{ pem: 'application/x-pem-file', der: 'application/x-x509-ca-cert', p7b: 'application/x-pkcs7-certificates' }`
- Implemented `convertCert({ file?, pemText?, targetFormat, password? }): Promise<FileResponse>`:
- Validates `targetFormat` against FORMAT_MIME (400 if unsupported)
- Input resolution: PEM text → `parsePemChain`; DER file → `forge.asn1.fromDer(toForgeBuffer(...))`; PFX → `pkcs12FromAsn1` + getBags; P7B → sniff + `messageFromPem`/`messageFromAsn1`
- PEM output: `certificateToPem(cert)` → `Buffer.from(str, 'utf-8').toString('base64')`
- DER output: `forge.util.bytesToHex(toDer(certificateToAsn1(cert)).getBytes())` → `Buffer.from(hex, 'hex').toString('base64')` — avoids Pitfall 1 / T-09-06
- P7B output: `pkcs7.createSignedData()` + `addCertificate(cert)` + `asn1.toDer(toAsn1())` + `pem.encode({ type:'PKCS7' })` → base64
- All forge ops in try/catch → BadRequestException
**`cert-manager.controller.ts`:**
- Added `@Body('pemText') pemText?: string` to `convertCert` endpoint
- Changed guard from "reject if no file" to "reject if no file AND no pemText"
- Passes `pemText` through to service
**Result: all 23 API tests pass (19 prior + 4 new convertCert).**
### Task 3: ConvertTab UI + convertCertAction + render tests
**`actions.ts`:**
- Added `FileResponse` interface (mirrors API response)
- Added `convertCertAction(input, targetFormat): Promise<FileResponse>` — builds FormData with file/pemText/password + targetFormat, delegates to `postForm('convert', form)`
**`components/ConvertTab.tsx`:**
- `'use client'` component with `useState` for loading/error/targetFormat
- Native `<select>` offering pem/der/p7b options (labels PEM/DER/P7B)
- Konvertieren button: disabled when no file or pemText or loading; label swaps to `t('actions.processing')`
- On success: calls `downloadBase64(response.filename, response.content, response.mimeType)` directly
- Error classification: 'password'/'passwort' → wrongPassword; 'format'/'invalid'/'unknown' → unknownFormat; else → generic
- Empty state shown when no error (empty state + format selector always visible together)
**`cert-manager.test.tsx`:**
- 3 new ConvertTab tests: format selector renders pem/der/p7b options; `downloadBase64` called on success (mocked via `vi.spyOn(actions, 'downloadBase64')`); text-destructive error on format rejection
- Import `ConvertTab` from components
**Result: all 14 cert-manager web tests pass (11 prior + 3 new ConvertTab).**
## Verification Results
| Check | Status | Notes |
|-------|--------|-------|
| `pnpm --filter @tessera/api exec vitest run cert-manager` | PASS | 23/23 tests |
| `pnpm --filter @tessera/web exec vitest run cert-manager` | PASS | 14/14 tests |
| `pnpm --filter @tessera/api exec tsc --noEmit` | PASS | 0 errors |
| `pnpm --filter @tessera/web exec tsc --noEmit` (prod files) | PASS | 0 errors in production files |
| DER path uses `bytesToHex → Buffer.from(hex,'hex') → base64` | PASS | T-09-06 compliant |
| `grep -q "convertCertAction" actions.ts` | PASS | Action wired |
| ConvertTab renders pem/der/p7b options | PASS | Verified by test |
| BadRequestException on malformed input | PASS | Verified by test |
**Note on web type-check:** Pre-existing `toBeInTheDocument` type augmentation errors (same as Plans 03–04) still present — not a regression. All production source files added in Plan 05 are type-clean.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] NotImplementedException import removed prematurely**
- **Found during:** Task 2 — tsc reported TS2304 (Cannot find name 'NotImplementedException')
- **Issue:** Removed `NotImplementedException` from import when cleaning up, but `mergeCerts` stub still uses it
- **Fix:** Re-added `NotImplementedException` to the import
- **Files modified:** `apps/api/src/cert-manager/cert-manager.service.ts`
- **Commit:** 5969464
**2. [Rule 1 - Bug] Test error message contained 'password' causing wrong error classification**
- **Found during:** Task 3 — ConvertTab error test used `'invalid format or wrong password'` which matched the 'password' branch before the 'invalid' branch
- **Issue:** Error classification checks 'password' first; message containing both keywords showed wrongPassword instead of unknownFormat
- **Fix:** Changed test error message to `'unknown format or invalid certificate'` (no 'password' substring)
- **Files modified:** `apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx`
- **Commit:** f89d656
## Known Stubs
| File | Stub | Reason |
|------|------|--------|
| `cert-manager.service.ts` | `mergeCerts` throws `NotImplementedException` | Implemented in Plan 06 (Merge/PFX slice) |
## Threat Model Compliance
| Threat ID | Status |
|-----------|--------|
| T-09-01 (malformed input → unhandled throw) | Mitigated — try/catch on all forge operations → BadRequestException |
| T-09-06 (binary encoding on DER output) | Mitigated — bytesToHex → Buffer.from(hex,'hex') → base64; never utf-8 round-trip |
| T-09-03 (oversized upload → OOM) | Mitigated — FileInterceptor fileSize 5MB (wired in Plan 01) |
| T-09-04 (unauthenticated cert processing) | Mitigated — global JwtAuthGuard + @UseModule guard (wired in Plan 01) |
## Self-Check: PASSED
| Check | Result |
|-------|--------|
| apps/api/src/cert-manager/cert-manager.service.ts | FOUND + MODIFIED |
| apps/api/src/cert-manager/cert-manager.controller.ts | FOUND + MODIFIED |
| apps/api/src/cert-manager/cert-manager.service.spec.ts | FOUND + MODIFIED |
| apps/web/src/app/(portal)/modules/cert-manager/actions.ts | FOUND + MODIFIED |
| apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx | FOUND + MODIFIED |
| apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx | FOUND + MODIFIED |
| Commit 37db58b (RED convertCert spec) | FOUND |
| Commit 5969464 (GREEN convertCert) | FOUND |
| Commit f89d656 (ConvertTab UI + tests) | FOUND |
| 23/23 API cert-manager tests passing | VERIFIED |
| 14/14 web cert-manager tests passing | VERIFIED |
| DER output uses bytesToHex→hex→base64 (not utf-8) | VERIFIED |
| convertCertAction in actions.ts | VERIFIED |
| BadRequestException on malformed input | VERIFIED |