161 lines
8.6 KiB
Markdown
161 lines
8.6 KiB
Markdown
---
|
|
phase: 09-cert-manager-module
|
|
plan: "04"
|
|
subsystem: api+frontend
|
|
tags: [cert-manager, splitCerts, node-forge, pkcs7, split-tab, tdd, vitest]
|
|
dependency_graph:
|
|
requires: [09-01, 09-02, 09-03]
|
|
provides: [cert-manager-split-endpoint, split-response-interface, split-tab-ui]
|
|
affects:
|
|
- apps/api/src/cert-manager/cert-manager.service.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/SplitTab.tsx
|
|
- apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
|
|
tech_stack:
|
|
added: []
|
|
patterns: [tdd-red-green, node-forge-pkcs7-split, parsePemChain-reuse, downloadBase64-pattern, vi.spyOn-mock]
|
|
key_files:
|
|
created: []
|
|
modified:
|
|
- apps/api/src/cert-manager/cert-manager.service.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/SplitTab.tsx
|
|
- apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
|
|
decisions:
|
|
- "splitCerts PEM path reuses parsePemChain helper (no duplication); P7B path sniffs first bytes for PEM vs DER"
|
|
- "format === 'crt' comparison removed — detectFormat only returns 'pem'|'der'|'pfx'|'p7b' (Rule 1 auto-fix, TS2367)"
|
|
- "SplitTab error classification: 'format'/'invalid'/'unknown' in message -> unknownFormat, else -> generic (mirrors InspectTab)"
|
|
- "splitCertsAction accepts File directly (not FormData) — caller is always browser File object; postForm wraps it"
|
|
metrics:
|
|
duration: "~4 minutes"
|
|
completed: "2026-07-02T05:19:00Z"
|
|
tasks_completed: 3
|
|
files_created: 0
|
|
files_modified: 5
|
|
requirements: [CERT-02]
|
|
status: complete
|
|
---
|
|
|
|
# Phase 09 Plan 04: Split Vertical Slice Summary
|
|
|
|
**One-liner:** splitCerts splits fullchain PEM chains and P7B PKCS7 bundles into individually downloadable base64 PEM certs; POST /split wired; SplitTab renders per-cert download list with subject CN and expiry.
|
|
|
|
## Objective
|
|
|
|
Second functional vertical slice: certificate chain/bundle splitting. Delivers CERT-02 (upload fullchain.pem or P7B, download each cert separately). Reuses parsePemChain + downloadBase64 patterns from Plan 03.
|
|
|
|
## Tasks Completed
|
|
|
|
| # | Name | Type | Commit | Status |
|
|
|---|------|------|--------|--------|
|
|
| 1 | RED — failing splitCerts spec | test | eec6631 | done |
|
|
| 2 | GREEN — implement splitCerts + wire POST /split | feat | 2c4ada3 | done |
|
|
| 3 | Split tab UI + splitCertsAction + render test | feat | 33b1bc3 | done |
|
|
|
|
## What Was Built
|
|
|
|
### Task 1: RED — failing splitCerts spec
|
|
|
|
Added 3 new splitCerts tests to `cert-manager.service.spec.ts`:
|
|
|
|
- **Fullchain PEM test:** builds two RSA-1024 self-signed certs, concatenates PEMs, calls splitCerts, asserts count=2, each cert content is base64 of single PEM block, both CNs present, ISO notAfter format
|
|
- **P7B bundle test:** builds a P7B PEM via `forge.pkcs7.createSignedData()` + `forge.pem.encode({ type:'PKCS7', body:... })`, calls splitCerts on a .p7b file, asserts at least 1 cert returned
|
|
- **Malformed input test:** garbage bytes with .pem extension → expects BadRequestException
|
|
|
|
All 3 tests FAIL against the NotImplementedException stub (RED confirmed). 16 prior tests remain green.
|
|
|
|
### Task 2: GREEN — splitCerts implementation
|
|
|
|
**`cert-manager.service.ts`:**
|
|
- Exported `SplitEntry` interface: `{ index, filename, content (base64 PEM), subject.cn, validity.notAfter }`
|
|
- Exported `SplitResponse` interface: `{ count, certs: SplitEntry[] }`
|
|
- Implemented `splitCerts({ file }): Promise<SplitResponse>`:
|
|
- PEM path: `parsePemChain(buffer.toString('utf-8'))` → array of forge certs
|
|
- P7B path: sniff first 27 bytes for `-----BEGIN` → `messageFromPem` (PEM-wrapped) or `asn1.fromDer` + `messageFromAsn1` (binary DER)
|
|
- Unsupported format (pfx/der) → explicit BadRequestException
|
|
- Each cert: `certificateToPem(cert)` → base64 → SplitEntry with index, filename `cert-N.pem`, subject.cn, notAfter
|
|
- Outer try/catch → BadRequestException on malformed input (T-09-01)
|
|
- POST /split controller route was already fully wired from Plan 01 (FileInterceptor, 5MB limit, T-09-03, T-09-04)
|
|
|
|
**Result: all 19 API tests pass (16 prior + 3 new splitCerts).**
|
|
|
|
### Task 3: SplitTab UI + splitCertsAction + render tests
|
|
|
|
**`actions.ts`:**
|
|
- Added `SplitEntry` + `SplitResponse` interfaces (mirrors API response)
|
|
- Added `splitCertsAction(file: File): Promise<SplitResponse>` — builds FormData, delegates to `postForm('split', form)`
|
|
|
|
**`components/SplitTab.tsx`:**
|
|
- `'use client'` component with `useState` for loading/result/error
|
|
- Aufteilen button: disabled when no file or loading; label swaps to `t('actions.processing')`
|
|
- Empty state when no result and no error
|
|
- Per-cert `<ul>` on success: each `<li>` shows subject.cn + validity.notAfter + Herunterladen button (bg-secondary) calling `downloadBase64(filename, content, 'application/x-pem-file')`
|
|
- Error in `text-sm text-destructive`: 'format'/'invalid'/'unknown' → unknownFormat, else → generic
|
|
|
|
**`cert-manager.test.tsx`:**
|
|
- 2 new SplitTab tests: success (mocked splitCertsAction resolves → 2 download buttons, 2 CN values shown) and error (rejects → text-destructive unknownFormat message)
|
|
- Import `SplitTab` from components; `vi.spyOn(actions, 'splitCertsAction')` pattern matching InspectTab
|
|
|
|
**Result: all 11 cert-manager web tests pass (9 prior + 2 new SplitTab).**
|
|
|
|
## Verification Results
|
|
|
|
| Check | Status | Notes |
|
|
|-------|--------|-------|
|
|
| `pnpm --filter @tessera/api exec vitest run` | PASS | 19/19 tests |
|
|
| `pnpm --filter @tessera/web exec vitest run cert-manager` | PASS | 11/11 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 |
|
|
| `grep -q "messageFromPem" cert-manager.service.ts` | PASS | P7B path present |
|
|
| `grep -q "splitCertsAction" actions.ts` | PASS | Action wired |
|
|
| SplitTab renders Herunterladen per cert | PASS | Verified by test |
|
|
| BadRequestException on malformed input | PASS | Verified by test |
|
|
|
|
**Note on web type-check:** Pre-existing 154 `toBeInTheDocument` type augmentation errors from Plan 03 still present — not a regression. All production source files added in Plan 04 are type-clean.
|
|
|
|
## Deviations from Plan
|
|
|
|
### Auto-fixed Issues
|
|
|
|
**1. [Rule 1 - Bug] TypeScript TS2367: comparison of 'pfx'|'p7b' with 'crt' has no overlap**
|
|
- **Found during:** Task 2 — `pnpm --filter @tessera/api exec tsc --noEmit` reported TS2367
|
|
- **Issue:** Condition `format === 'pem' || format === 'der' || format === 'crt'` — `detectFormat` return type is `'pem'|'der'|'pfx'|'p7b'`. After narrowing out 'pem' and 'der', TypeScript flags `'pfx'|'p7b' === 'crt'` as unintentional.
|
|
- **Fix:** Changed condition to `format === 'pem'` only (DER is a single-cert binary not a chain; falls through to the `else` BadRequestException path which is correct behavior).
|
|
- **Files modified:** `apps/api/src/cert-manager/cert-manager.service.ts`
|
|
- **Commit:** 2c4ada3
|
|
|
|
## Known Stubs
|
|
|
|
| File | Stub | Reason |
|
|
|------|------|--------|
|
|
| `cert-manager.service.ts` | `mergeCerts` throws `NotImplementedException` | Implemented in Plan 05 (Merge/PFX slice) |
|
|
| `cert-manager.service.ts` | `convertCert` throws `NotImplementedException` | Implemented in Plan 06 (Convert slice) |
|
|
|
|
## Threat Model Compliance
|
|
|
|
| Threat ID | Status |
|
|
|-----------|--------|
|
|
| T-09-01 (malformed bundle → unhandled throw) | Mitigated — outer try/catch on all forge operations → BadRequestException |
|
|
| T-09-03 (oversized upload → OOM) | Mitigated — FileInterceptor fileSize 5MB (wired in Plan 01) |
|
|
| T-09-04 (unauthenticated cert processing) | Mitigated — credentials:'include' in postForm; ModuleGuard server-side (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.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/SplitTab.tsx | FOUND + MODIFIED |
|
|
| apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx | FOUND + MODIFIED |
|
|
| Commit eec6631 (RED splitCerts spec) | FOUND |
|
|
| Commit 2c4ada3 (GREEN splitCerts) | FOUND |
|
|
| Commit 33b1bc3 (SplitTab UI + tests) | FOUND |
|
|
| 19/19 API cert-manager tests passing | VERIFIED |
|
|
| 11/11 web cert-manager tests passing | VERIFIED |
|
|
| messageFromPem in service (P7B path) | VERIFIED |
|
|
| splitCertsAction in actions.ts | VERIFIED |
|
|
| BadRequestException on malformed | VERIFIED |
|