12 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics, requirements, status
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | requirements | status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-cert-manager-module | 06 | api+frontend |
|
|
|
|
|
|
|
complete |
Phase 09 Plan 06: Merge + PFX Vertical Slice Summary
One-liner: mergeCerts produces PEM chains and password-protected cert-only PFX bundles via toPkcs12Asn1(null, certs, password) — Open Question 1 confirmed working in node-forge 1.4.0; Merge tab with multi-file list, output selector, and shared PasswordField integration completes the cert-manager vertical stack.
Objective
Final vertical slice: merge multiple certificates into a PEM chain or password-protected PFX/PKCS12 bundle. Implements CERT-03 and the write half of CERT-05. Resolves RESEARCH Open Question 1 (null-key PFX creation) during implementation.
Tasks Completed
| # | Name | Type | Commit | Status |
|---|---|---|---|---|
| 1 | RED — failing mergeCerts spec (PEM chain + password-PFX round-trip) | test | f43e92c |
done |
| 2 | GREEN — implement mergeCerts + PFX-create + wire POST /merge | feat | 6326064 |
done |
| 3 | Merge tab UI + PFX convert option + render tests | feat | 8b15a00 |
done |
What Was Built
Task 1: RED — failing mergeCerts spec
Added 4 new mergeCerts tests to cert-manager.service.spec.ts:
- PEM chain (2 files): asserts
base64→decodedcontains exactly 2 BEGIN CERTIFICATE blocks, mimeTypeapplication/x-pem-file - Password-PFX round-trip: calls
mergeCerts({ files:[1 file], outputFormat:'pfx', password:'secret' }), decodes base64 PFX, re-parses viapkcs12FromAsn1(p12Asn1, 'secret'), asserts cert bags present (proves password-protected and correct) - Missing PFX password: asserts
BadRequestExceptionwhenpasswordis absent on PFX output - Garbage input: asserts
BadRequestExceptionfor malformed file buffers
Note: 2-file minimum tested at controller level (existing guard files.length < 2); service tests use 1+ files directly.
All 4 fail against NotImplementedException stub (RED confirmed). Prior 23 tests remain green.
Task 2: GREEN — mergeCerts + PFX-create + convertCert pfx output
cert-manager.service.ts:
- Replaced
mergeCertsstub with full implementation:- Parses each file using
detectFormat→ PEM viaparsePemChain; DER viaasn1.fromDer(toForgeBuffer); PFX viapkcs12FromAsn1; P7B via sniff +messageFromPem/messageFromAsn1 - PFX output requires non-empty password → BadRequestException if missing (T-09-02)
- PEM output:
certificateToPem()concatenation →Buffer.from(chain,'utf-8').toString('base64')→ FileResponsechain.pem - PFX output:
toPkcs12Asn1(null, certs, password, {algorithm:'3des'})→bytesToHex→Buffer.from(hex,'hex')→ base64 → FileResponsebundle.pfx(Pitfall 1 avoidance) - All forge calls in try/catch → BadRequestException; password never logged (T-09-02)
- Parses each file using
convertCertgains PFX output target (reuses same null-key pattern, single cert)FORMAT_MIMEextended withpfx: 'application/x-pkcs12'NotImplementedExceptionremoved from import (no longer used)
Open Question 1 resolution: forge.pkcs12.toPkcs12Asn1(null as any, certs, password, {algorithm:'3des'}) works correctly in node-forge 1.4.0. Null private key produces a cert-only PKCS12 bundle (cert bag only, no key bag). No fallback to lower-level certBag construction was needed.
Result: 27/27 API tests pass (23 prior + 4 new mergeCerts); tsc --noEmit exits 0.
Task 3: MergeTab UI + ConvertTab pfx + page.tsx update + render tests
actions.ts:
- Added
mergeCertsAction(files: File[], outputFormat: string, password?: string): Promise<FileResponse>— builds FormData withfiles.forEach(f => form.append('files', f)), appends outputFormat and optional password, delegates topostForm('merge', form)(T-09-02/T-09-04)
components/MergeTab.tsx (fully replaced stub):
useState<File[]>for local file list (separate from shared DropZone)data-testid="merge-file-input"native file input withmultiple+ all cert extensions- Output selector: pem ('PEM-Kette') / pfx ('PFX / PKCS12')
data-testid="merge-action-btn"Zusammenfuehren button disabled whenfiles.length < 2onOutputFormatChange?: (format: string) => voidcallback to notify page.tsx for shared PasswordField visibility- On success:
downloadBase64(filename, content, mimeType)(T-09-02) - Error classification: wrongPassword / unknownFormat / generic
components/ConvertTab.tsx:
- Added
pfxoption to format selector ('PFX / PKCS12') - Added
onTargetFormatChange?: (format: string) => voidprop (same callback pattern) - Type
TargetFormat = 'pem' | 'der' | 'p7b' | 'pfx'
page.tsx:
- Lifts
mergeOutputFormat+convertOutputFormatstate (both default 'pem') showPasswordupdated: true when PFX file selected OR active-merge-tab pfx OR active-convert-tab pfx- Passes
onOutputFormatChange={setMergeOutputFormat}to MergeTab - Passes
onTargetFormatChange={setConvertOutputFormat}to ConvertTab - MergeTab receives only
password(not file/pemText which are irrelevant for merge)
cert-manager.test.tsx:
- 5 new tests:
- MergeTab action button disabled with 0 files
- MergeTab action button enabled after 2 files added via
fireEvent.change - Shared PasswordField appears in page.tsx when PFX output selected in MergeTab
mergeCertsActionmocked →downloadBase64called on merge success- ConvertTab selector contains all 4 options including pfx
Result: 19/19 web cert-manager tests pass (14 prior + 5 new); all production cert-manager files type-clean.
Verification Results
| Check | Status | Notes |
|---|---|---|
pnpm --filter @tessera/api exec vitest run cert-manager |
PASS | 27/27 tests |
pnpm --filter @tessera/web exec vitest run cert-manager |
PASS | 19/19 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 |
Full web suite vitest run |
PARTIAL | 102/107 pass — 5 pre-existing dashboard failures (Phase 8, out of scope) |
grep -q "toPkcs12Asn1" |
PASS | Open Question 1 resolved |
grep -q "length < 2" controller |
PASS | 2-file minimum at controller level |
mergeCertsAction in actions.ts |
PASS | Action wired |
| Merge button disabled < 2 files | PASS | Verified by test |
| Password field shown on PFX output | PASS | Verified by integration test |
| ConvertTab includes pfx option | PASS | Verified by test |
Open Question 1 Resolution
Question: Does forge.pkcs12.toPkcs12Asn1(null, certs, password) work with null private key?
Result: YES — works as expected in node-forge 1.4.0.
toPkcs12Asn1(null as any, certs, password!, { algorithm: '3des' }) produces a valid PKCS12 file containing only a cert bag (no key bag). The produced PFX:
- Opens correctly with
pkcs12FromAsn1(p12Asn1, 'secret')with the correct password - Rejects with a forge exception on wrong password (verified by round-trip test)
- Contains all provided certificates in the cert bag
No fallback to lower-level forge.pkcs12 certBag API construction was needed. The Pitfall 3 concern from RESEARCH.md did not materialize with node-forge 1.4.0.
Deviations from Plan
Auto-fixed Issues
None — plan executed exactly as written.
Known Stubs
None — mergeCerts is now fully implemented. All four cert-manager service methods are complete.
Deferred Items (Out of Scope — Phase 8)
Pre-existing dashboard test failures (NOT caused by this plan):
dashboard-grid.test.tsx: 2 failures — react-grid-layout mock missingnoCompactorexportnote-widget.test.tsx: 3 failures — fetch assertion errors (hideToolbar-Prop issue from 01.07)
These were tracked in M git status before plan execution. Logged to context for Phase 8 cleanup.
Threat Model Compliance
| Threat ID | Status |
|---|---|
| T-09-01 (malformed input → unhandled throw) | Mitigated — try/catch on all forge operations in mergeCerts → BadRequestException |
| T-09-02 (PFX password handling) | Mitigated — password never logged, only used in toPkcs12Asn1 MAC; never in filename or response |
| T-09-03 (multi-upload DoS) | Mitigated — FilesInterceptor maxCount 20 + fileSize 5 MB (wired in Plan 01) |
| T-09-04 (unauthenticated) | Mitigated — global JwtAuthGuard + @UseModule('cert-manager') guard (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/MergeTab.tsx | FOUND + MODIFIED |
| apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx | FOUND + MODIFIED |
| apps/web/src/app/(portal)/modules/cert-manager/page.tsx | FOUND + MODIFIED |
| apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx | FOUND + MODIFIED |
Commit f43e92c (RED mergeCerts spec) |
FOUND |
Commit 6326064 (GREEN mergeCerts + pfx) |
FOUND |
Commit 8b15a00 (MergeTab UI + tests) |
FOUND |
| 27/27 API cert-manager tests passing | VERIFIED |
| 19/19 web cert-manager tests passing | VERIFIED |
| toPkcs12Asn1 in service | VERIFIED |
| 2-file guard in controller | VERIFIED |
| mergeCertsAction in actions.ts | VERIFIED |
| PFX option in ConvertTab | VERIFIED |
| Merge button disabled < 2 files | VERIFIED |
| Password field on PFX output | VERIFIED |