185 lines
14 KiB
Markdown
185 lines
14 KiB
Markdown
---
|
||
phase: 09-cert-manager-module
|
||
plan: 03
|
||
type: execute
|
||
wave: 2
|
||
depends_on: [09-01, 09-02]
|
||
files_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/InspectTab.tsx
|
||
- apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
|
||
autonomous: true
|
||
requirements: [CERT-01, CERT-05]
|
||
|
||
must_haves:
|
||
truths:
|
||
- "A user uploads (or pastes) a PEM/DER/PFX/P7B certificate and sees subject, issuer, validity, SANs, key type/size, serial, signature algorithm, and SHA-1 + SHA-256 fingerprints"
|
||
- "A password-protected PFX is parsed when the correct password is supplied; a wrong password returns HTTP 400 (not 500)"
|
||
- "The Inspect tab renders a key-value result grid on success and a localized error on failure"
|
||
artifacts:
|
||
- "CertManagerService.parseCert implemented (PEM/DER/PFX/P7B -> CertDetails)"
|
||
- "POST /modules/cert-manager/parse wired to parseCert"
|
||
- "InspectTab.tsx renders the CertDetails grid + inspect action"
|
||
key_links:
|
||
- "InspectTab -> inspectCertAction -> POST /modules/cert-manager/parse -> CertManagerService.parseCert"
|
||
- "parseCert wraps node-forge in try/catch -> BadRequestException (wrong password / malformed)"
|
||
---
|
||
|
||
<objective>
|
||
First functional vertical slice: certificate inspection. Implement CertManagerService.parseCert to accept an uploaded file (PEM/DER/PFX/P7B) or pasted PEM text plus an optional PFX password, and return structured CertDetails. Wire the POST /parse endpoint and build the Inspect tab to render the result grid.
|
||
|
||
MVP: after this plan a user can activate the module, upload a cert, and read its parsed details — a complete end-to-end capability (CERT-01). Password-protected PFX open (CERT-05 read half) is covered because parsing a .pfx requires the supplied password.
|
||
|
||
Purpose: Delivers CERT-01 and the read half of CERT-05; establishes the parse-and-render pattern reused by later slices.
|
||
Output: Working Inspect tab end-to-end + tested parseCert service.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||
@$HOME/.claude/gsd-core/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@.planning/ROADMAP.md
|
||
@.planning/STATE.md
|
||
@.planning/phases/09-cert-manager-module/09-CONTEXT.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-01-SUMMARY.md
|
||
@.planning/phases/09-cert-manager-module/09-02-SUMMARY.md
|
||
</context>
|
||
|
||
<artifacts>
|
||
## Artifacts this plan produces
|
||
|
||
- Implemented method: `CertManagerService.parseCert({ file?, pemText?, password? }): CertDetails`
|
||
- New TS interface: `CertDetails` (subject, issuer, validity{notBefore,notAfter,isExpired,daysLeft}, san[], keyType, keyBits, serialNumber, signatureAlgorithm, fingerprint{sha1,sha256}, pemPreview) — per RESEARCH Inspect Response Shape
|
||
- Wired route: `POST /modules/cert-manager/parse` (FileInterceptor('file') + @Body pemText/password)
|
||
- New action: `inspectCertAction(input)` in actions.ts (JSON path for pemText, multipart path for file)
|
||
- Implemented component: `InspectTab` (key-value result grid + inspect button + loading/error)
|
||
</artifacts>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 1: RED — failing parseCert spec</name>
|
||
<files>apps/api/src/cert-manager/cert-manager.service.spec.ts</files>
|
||
<read_first>
|
||
- apps/api/src/cert-manager/cert-manager.service.spec.ts (existing helper/seed tests + beforeAll self-signed cert generator from Plan 01)
|
||
- apps/api/src/cert-manager/cert-manager.service.ts (current parseCert stub throwing NotImplementedException + helpers)
|
||
- .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 node-forge parse APIs; Inspect Response Shape; Pitfall 1 binary encoding)
|
||
</read_first>
|
||
<behavior>
|
||
- Test: parseCert({ pemText: <self-signed PEM> }) returns CertDetails with subject.cn matching the generated CN, fingerprint.sha256 matching /^[0-9A-F]{2}(:[0-9A-F]{2})+$/, keyType 'RSA', keyBits 2048, and validity.isExpired false.
|
||
- Test: parseCert({ file: { originalname:'c.der', buffer: <DER of the cert> } }) returns the same subject.cn (DER path uses 'binary' encoding).
|
||
- Test: parseCert({ file: { originalname:'c.pfx', buffer: <PFX built with password 'secret'> }, password: 'secret' }) returns CertDetails for the enclosed cert.
|
||
- Test: parseCert({ file: { originalname:'c.pfx', buffer: <same PFX> }, password: 'wrong' }) throws BadRequestException (asserted via rejects.toThrow / expect(() => ...).toThrow with the Nest exception).
|
||
- Test: parseCert({ pemText: 'not a cert' }) throws BadRequestException.
|
||
</behavior>
|
||
<action>
|
||
Extend cert-manager.service.spec.ts with the Behavior tests above. Build the DER and password-protected PFX fixtures in the spec from the beforeAll self-signed cert using node-forge (forge.asn1.toDer + forge.pkcs12.toPkcs12Asn1 with password 'secret'). Run the suite and confirm these new tests FAIL against the current parseCert stub (RED). Do not implement parseCert in this task.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/api test cert-manager --run 2>&1 | grep -Eiq 'fail|NotImplemented|✗|×' && echo RED_CONFIRMED</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- New parseCert tests exist in cert-manager.service.spec.ts covering PEM, DER, PFX-correct-password, PFX-wrong-password (BadRequestException), and malformed input
|
||
- Running the suite shows the parseCert tests failing (RED) while the Plan 01 helper/seed tests still pass
|
||
</acceptance_criteria>
|
||
<done>Failing parseCert spec committed (RED) covering all input formats + wrong-password + malformed cases.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 2: GREEN — implement parseCert + wire POST /parse</name>
|
||
<files>apps/api/src/cert-manager/cert-manager.service.ts, apps/api/src/cert-manager/cert-manager.controller.ts</files>
|
||
<read_first>
|
||
- apps/api/src/cert-manager/cert-manager.service.ts (helpers detectFormat/toForgeBuffer/getFingerprint/parsePemChain from Plan 01)
|
||
- apps/api/src/cert-manager/cert-manager.controller.ts (parse route stub + FileInterceptor from Plan 01)
|
||
- apps/api/src/cert-manager/cert-manager.service.spec.ts (the RED tests from Task 1 — target contract)
|
||
- .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 full node-forge API: certificateFromPem, fromDer, pkcs12FromAsn1, subject/issuer getField, SAN extraction, RSA bitLength; Inspect Response Shape)
|
||
</read_first>
|
||
<action>
|
||
Implement CertManagerService.parseCert to: resolve input (pemText -> parse as PEM/chain; file -> detectFormat, then PEM via certificateFromPem, DER via asn1.fromDer(toForgeBuffer(...)) + certificateFromAsn1, PFX via pkcs12FromAsn1(asn1, password ?? '') then extract certBag, P7B via messageFromPem or messageFromAsn1 depending on content sniff). Build CertDetails: subject/issuer CN/O/OU/C via cert.subject.getField / cert.issuer.getField; validity.notBefore/notAfter from cert.validity, isExpired and daysLeft computed against now; san[] from the subjectAltName extension; keyType 'RSA'/'EC' and keyBits from the public key bitLength; serialNumber; signatureAlgorithm from the cert; fingerprint.sha1 and .sha256 via getFingerprint; pemPreview via certificateToPem. Wrap ALL node-forge calls in try/catch and throw BadRequestException with a generic message on failure (covers malformed cert AND wrong PFX password -> 400, threat T-09-01/T-09-02). Never log the password.
|
||
Define and export the CertDetails interface (co-located in the service or a types file).
|
||
In cert-manager.controller.ts, ensure POST parse uses FileInterceptor('file', { limits: { fileSize: 5*1024*1024 } }), reads @Body('pemText') and @Body('password'), rejects when neither file nor pemText present (BadRequestException), and delegates to parseCert. Run the suite until all parseCert tests pass (GREEN).
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/api test cert-manager --run</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `pnpm --filter @tessera/api test cert-manager --run` exits 0 with all parseCert tests passing
|
||
- `grep -q "BadRequestException" apps/api/src/cert-manager/cert-manager.service.ts` in the parseCert catch path
|
||
- No `console.log`/logger call in the service references the password value (grep shows no `password` argument passed to logger)
|
||
- `grep -q "fileSize: 5" apps/api/src/cert-manager/cert-manager.controller.ts`
|
||
- `pnpm --filter @tessera/api type-check` exits 0
|
||
</acceptance_criteria>
|
||
<done>parseCert returns full CertDetails for PEM/DER/PFX/P7B, throws 400 on wrong password/malformed input, and POST /parse is wired; API tests green.</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 3: Inspect tab UI + action + render test</name>
|
||
<files>apps/web/src/app/(portal)/modules/cert-manager/actions.ts, apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx, apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx</files>
|
||
<read_first>
|
||
- apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx (empty-state stub from Plan 02)
|
||
- apps/web/src/app/(portal)/modules/cert-manager/actions.ts (API_URL, postForm, downloadBase64 from Plan 02)
|
||
- apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx (shell tests + i18n wiring from Plan 02)
|
||
- apps/web/src/app/(portal)/modules/domaincheck/page.tsx (loading/error state pattern)
|
||
- .planning/phases/09-cert-manager-module/09-UI-SPEC.md (Analysieren result = key-value grid grid-cols-2 gap-2 text-sm; loading label swap; error text-destructive)
|
||
</read_first>
|
||
<action>
|
||
Add inspectCertAction to actions.ts: if pemText is present, POST JSON { pemText, password } to /modules/cert-manager/parse with Content-Type application/json; else build FormData with file + optional password and use the postForm('parse', form) helper. Return the parsed CertDetails JSON; throw on !ok (reuse postForm error behavior for the multipart path).
|
||
Implement InspectTab: accept { file, pemText, password }. Render a primary 'Analysieren' button (t('actions.inspect'), disabled + label t('actions.processing') while loading, per UI-SPEC). On click call inspectCertAction and store the result; on error store a localized message (wrong-password -> t('error.wrongPassword'), unknown format -> t('error.unknownFormat'), else t('error.generic')). Render the empty state (t('emptyState.inspect')) when no result; render a grid grid-cols-2 gap-2 text-sm of subject/issuer/validity/SANs/keyType/keyBits/serial/signatureAlgorithm/fingerprint.sha1/fingerprint.sha256 on success; render error in text-sm text-destructive. All labels via t(). No shadcn.
|
||
Extend cert-manager.test.tsx with a test that mocks inspectCertAction to resolve a CertDetails object and asserts the InspectTab renders the subject CN and the sha256 fingerprint after clicking Analysieren; and a test that mocks a rejection and asserts a text-destructive error is shown.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/web test cert-manager --run</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `grep -q "inspectCertAction" apps/web/src/app/(portal)/modules/cert-manager/actions.ts`
|
||
- InspectTab shows the empty state before a result and a key-value grid (grid-cols-2) after a successful inspect
|
||
- `pnpm --filter @tessera/web test cert-manager --run` exits 0 including the new success + error InspectTab tests
|
||
- `pnpm --filter @tessera/web type-check` exits 0
|
||
</acceptance_criteria>
|
||
<done>Inspect tab loads a cert end-to-end, renders the details grid on success and a localized destructive error on failure; web tests green.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<threat_model>
|
||
## Trust Boundaries
|
||
|
||
| Boundary | Description |
|
||
|----------|-------------|
|
||
| client -> API /parse | Untrusted cert bytes + optional PFX password enter node-forge parsing |
|
||
|
||
## STRIDE Threat Register
|
||
|
||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||
| T-09-01 | Tampering | CertManagerService.parseCert (node-forge) | medium | mitigate | Every node-forge call wrapped in try/catch; malformed cert -> BadRequestException (400), never an unhandled 500 |
|
||
| T-09-02 | Information Disclosure | parseCert password handling | high | mitigate | Wrong PFX password caught -> generic 400 message; password value never logged and never echoed in the response |
|
||
| T-09-03 | Denial of Service | POST /parse upload | high | mitigate | FileInterceptor `limits.fileSize` = 5 MB caps in-memory buffer |
|
||
| T-09-04 | Elevation of Privilege | POST /parse | high | mitigate | Global JwtAuthGuard + `@UseModule('cert-manager')` on the controller |
|
||
</threat_model>
|
||
|
||
<verification>
|
||
- `pnpm --filter @tessera/api test cert-manager --run` — parseCert suite green (all formats + wrong password 400)
|
||
- `pnpm --filter @tessera/web test cert-manager --run` — InspectTab success + error tests green
|
||
- `pnpm --filter @tessera/api type-check` and `pnpm --filter @tessera/web type-check` clean
|
||
- Manual (phase gate): upload a real cert, verify grid; upload a password PFX with wrong then right password
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- parseCert returns full CertDetails for PEM/DER/PFX/P7B (CERT-01) and opens password PFX with correct password / 400 on wrong (CERT-05 read)
|
||
- Inspect tab works end-to-end with localized errors
|
||
- All API + web tests green; type-checks clean
|
||
</success_criteria>
|
||
|
||
<output>
|
||
Create `.planning/phases/09-cert-manager-module/09-03-SUMMARY.md` when done
|
||
</output>
|