Files

14 KiB
Raw Permalink Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
09-cert-manager-module 06 execute 5
09-05
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/MergeTab.tsx
apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx
apps/web/src/app/(portal)/modules/cert-manager/page.tsx
apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
true
CERT-03
CERT-04
CERT-05
truths artifacts key_links
A user uploads two or more certificates and downloads them merged as a single PEM chain
A user merges certs into a password-protected PFX/PKCS12 bundle by supplying a password; the resulting PFX opens with that password
The merge button is disabled until at least two files are selected; the password field appears when PFX output is chosen
CertManagerService.mergeCerts implemented (PEM chain + PFX create with password)
POST /modules/cert-manager/merge wired to mergeCerts (FilesInterceptor)
MergeTab.tsx (multi-file + output selector + conditional password) and PFX output option added to ConvertTab
MergeTab -> mergeCertsAction(files, outputFormat, password) -> POST /modules/cert-manager/merge -> CertManagerService.mergeCerts
outputFormat 'pfx' -> forge.pkcs12.toPkcs12Asn1 with password -> base64 PFX -> downloadBase64
Final vertical slice: merge multiple certificates into a PEM chain or a password-protected PFX/PKCS12 bundle. Implement CertManagerService.mergeCerts (multi-file), wire POST /merge with FilesInterceptor, build the Merge tab (multi-file upload, output-format selector, conditional password field), and add PFX as an output option to the Convert tab.

MVP: after this plan a user can combine certs into a chain or a password PFX and download it — completing CERT-03 and the write half of CERT-05.

Purpose: Delivers CERT-03 and CERT-05 (PFX create); resolves RESEARCH Open Question 1 (null-key PFX) during implementation. Output: Working Merge tab end-to-end + tested mergeCerts service + PFX convert option.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/09-cert-manager-module/09-RESEARCH.md @.planning/phases/09-cert-manager-module/09-PATTERNS.md @.planning/phases/09-cert-manager-module/09-UI-SPEC.md @.planning/phases/09-cert-manager-module/09-05-SUMMARY.md ## Artifacts this plan produces
  • Implemented method: CertManagerService.mergeCerts({ files, outputFormat, password? }): FileResponse
  • PFX-create helper: cert(s) -> forge.pkcs12.toPkcs12Asn1 (cert-only, null-key path resolved per Open Question 1) -> base64
  • Wired route: POST /modules/cert-manager/merge (FilesInterceptor('files', 20) + @Body outputFormat/password)
  • New action: mergeCertsAction(files, outputFormat, password?) in actions.ts
  • Implemented component: MergeTab (multi-file list + output selector + conditional password + download)
  • ConvertTab gains a 'pfx' output option (reuses PFX-create + password field)
Task 1: RED — failing mergeCerts spec (PEM chain + password PFX) 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 (mergeCerts stub + parse helpers) - .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 certificateToPem concat + pkcs12.toPkcs12Asn1; Pitfall 3 null-key PFX; Open Question 1) - Test: mergeCerts({ files: [ {buffer: certA PEM}, {buffer: certB PEM} ], outputFormat: 'pem' }) returns FileResponse whose base64 content decodes to a PEM containing exactly two BEGIN CERTIFICATE blocks, mimeType application/x-pem-file. - Test: mergeCerts({ files: [ {buffer: certA PEM} ], outputFormat: 'pfx', password: 'secret' }) returns a PFX whose base64 content, re-parsed via pkcs12FromAsn1 with password 'secret', yields the enclosed cert (round trip); mimeType application/x-pkcs12. - Test: mergeCerts({ files: [singleFile], outputFormat: 'pem' }) is allowed by the service (the 2-file minimum is enforced at the controller); OR assert controller-level guard separately — document which layer enforces the minimum. - Test: mergeCerts({ files: [ {buffer: garbage} ], outputFormat: 'pem' }) throws BadRequestException. Add the Behavior tests to cert-manager.service.spec.ts using the two self-signed cert fixtures. For the PFX test, re-open the produced bundle with pkcs12FromAsn1 + password 'secret' to prove it is password-protected and round-trips. Confirm RED against the mergeCerts stub. Do not implement mergeCerts here. pnpm --filter @tessera/api test cert-manager --run 2>&1 | grep -Eiq 'fail|NotImplemented|✗|×' && echo RED_CONFIRMED - mergeCerts tests exist for PEM-chain (2 certs), password-PFX round trip, and malformed input - Suite shows mergeCerts tests failing while all prior tests pass Failing mergeCerts spec committed (RED) including a password-PFX round-trip assertion. Task 2: GREEN — implement mergeCerts + PFX-create + wire POST /merge 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, toForgeBuffer, convertCert serialization) - apps/api/src/cert-manager/cert-manager.controller.ts (merge route stub + FilesInterceptor from Plan 01) - apps/api/src/cert-manager/cert-manager.service.spec.ts (RED contract) - .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 toPkcs12Asn1; Pitfall 3 + Open Question 1 null-key handling) Implement CertManagerService.mergeCerts({ files, outputFormat, password }): parse each file's buffer to a forge cert (reuse the shared input-resolution helper). For outputFormat 'pem': concatenate certificateToPem(cert) for all certs -> FileResponse { filename 'chain.pem', content base64(utf-8), mimeType application/x-pem-file }. For outputFormat 'pfx': build the PKCS12 via forge.pkcs12.toPkcs12Asn1 with the supplied password (require a non-empty password for PFX output -> BadRequestException if missing). Resolve Open Question 1: attempt cert-only creation with a null private key; if node-forge throws (Pitfall 3), fall back to the lower-level certBag-only construction. Serialize -> bytesToHex -> Buffer -> base64 -> FileResponse { filename 'bundle.pfx', mimeType application/x-pkcs12 }. Wrap all forge calls in try/catch -> BadRequestException; never log the password. In the controller, POST merge uses FilesInterceptor('files', 20, { limits: { fileSize: 5*1024*1024 } }), reads @Body('outputFormat') and @Body('password'), rejects when files.length < 2 (BadRequestException), and delegates. Run suite to GREEN. Note the Open Question 1 resolution (null-key worked vs. fallback used) in the SUMMARY. pnpm --filter @tessera/api test cert-manager --run - `pnpm --filter @tessera/api test cert-manager --run` exits 0 with mergeCerts tests passing including the password-PFX round trip - `grep -q "toPkcs12Asn1" apps/api/src/cert-manager/cert-manager.service.ts` - Missing password on PFX output returns BadRequestException (asserted in spec) - `grep -q "length < 2" apps/api/src/cert-manager/cert-manager.controller.ts` (or equivalent 2-file guard) - `pnpm --filter @tessera/api type-check` exits 0 mergeCerts produces a PEM chain and a password-protected PFX that round-trips; POST /merge wired with a 2-file minimum; API tests green. Task 3: Merge tab UI (multi-file + password) + PFX convert option + render tests apps/web/src/app/(portal)/modules/cert-manager/actions.ts, apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.tsx, apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx, apps/web/src/app/(portal)/modules/cert-manager/page.tsx, apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx - apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.tsx (empty-state stub) - apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx (format selector from Plan 05 — add 'pfx' option) - apps/web/src/app/(portal)/modules/cert-manager/page.tsx (shared PasswordField show-condition — extend for PFX output on merge/convert) - 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 (Zusammenfuehren = multi-file list + output selector + single download; merge button disabled < 2 files; password field appears when PFX output selected) Add mergeCertsAction(files, outputFormat, password?) to actions.ts: build FormData appending each file under field 'files', plus outputFormat and optional password; call postForm('merge', form); return FileResponse. Implement MergeTab: accept the shared password value; maintain a local list of selected files (multi-select via a file input allowing multiple, or repeated DropZone adds). Render an output-format selector (pem | pfx). The primary 'Zusammenfuehren' button (t('actions.merge'), loading label swap) is disabled until files.length >= 2 (per UI-SPEC). When output is 'pfx', ensure the shared password field is shown (update the page.tsx show-condition so PasswordField appears when the active tab's chosen output is PFX). On success call downloadBase64(filename, content, mimeType). Empty state t('emptyState.merge'); localized errors in text-destructive. Update page.tsx PasswordField show-condition: show when the selected file is .pfx/.p12 (existing) OR the active MergeTab/ConvertTab output format is 'pfx'. Add a 'pfx' option to ConvertTab's selector and pass the password through to convertCertAction so Convert can also emit a password PFX (reuses the same backend path — Convert with target 'pfx' may route through convertCert delegating to the PFX-create helper, or document that PFX convert uses the merge/PFX helper). Extend cert-manager.test.tsx: assert the Zusammenfuehren button is disabled with fewer than two files and enabled with two; assert the password field becomes visible when PFX output is selected; mock mergeCertsAction to resolve a FileResponse and assert downloadBase64 is invoked. pnpm --filter @tessera/web test cert-manager --run - `grep -q "mergeCertsAction" apps/web/src/app/(portal)/modules/cert-manager/actions.ts` - Merge button is disabled when fewer than 2 files are selected and enabled at 2 (asserted in test) - Password field is shown when PFX output is selected (asserted in test) - ConvertTab selector now includes a 'pfx' option - `pnpm --filter @tessera/web test cert-manager --run` exits 0 including the new MergeTab tests - `pnpm --filter @tessera/web type-check` exits 0 Merge tab combines >=2 certs into a PEM chain or password PFX end-to-end; PFX output option added to Convert; web tests green.

<threat_model>

Trust Boundaries

Boundary Description
client -> API /merge Multiple untrusted cert files + PFX password enter node-forge

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-09-01 Tampering mergeCerts (node-forge parse + pkcs12) medium mitigate try/catch around parse + toPkcs12Asn1 -> BadRequestException; missing PFX password rejected as 400
T-09-02 Information Disclosure PFX password handling high mitigate Password used only to build the PKCS12 MAC; never logged, never returned in the response, never in the filename
T-09-03 Denial of Service POST /merge multi-upload high mitigate FilesInterceptor maxCount 20 + limits.fileSize = 5 MB per file caps total memory
T-09-04 Elevation of Privilege POST /merge high mitigate Global JwtAuthGuard + @UseModule('cert-manager')
</threat_model>
- `pnpm --filter @tessera/api test cert-manager --run` — mergeCerts green incl. password-PFX round trip - `pnpm --filter @tessera/web test cert-manager --run` — MergeTab disabled/enabled + password-visibility + download tests green - `pnpm --filter @tessera/api test --run && pnpm --filter @tessera/web test --run` — full phase suite green (phase gate) - type-checks clean - Manual (phase gate): merge two real certs to a PFX with a password, re-upload to Inspect with that password, confirm it opens

<success_criteria>

  • mergeCerts produces PEM chains and password-protected PFX bundles (CERT-03 + CERT-05 write)
  • Merge tab + PFX convert option work end-to-end with conditional password field
  • Full API + web suites green; type-checks clean </success_criteria>
Create `.planning/phases/09-cert-manager-module/09-06-SUMMARY.md` when done