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