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
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.
Task 1: RED — failing parseCert spec
apps/api/src/cert-manager/cert-manager.service.spec.ts
- 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)
- Test: parseCert({ pemText: }) 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: } }) returns the same subject.cn (DER path uses 'binary' encoding).
- Test: parseCert({ file: { originalname:'c.pfx', buffer: }, password: 'secret' }) returns CertDetails for the enclosed cert.
- Test: parseCert({ file: { originalname:'c.pfx', buffer: }, password: 'wrong' }) throws BadRequestException (asserted via rejects.toThrow / expect(() => ...).toThrow with the Nest exception).
- Test: parseCert({ pemText: 'not a cert' }) throws BadRequestException.
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.
pnpm --filter @tessera/api test cert-manager --run 2>&1 | grep -Eiq 'fail|NotImplemented|✗|×' && echo RED_CONFIRMED
- 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
Failing parseCert spec committed (RED) covering all input formats + wrong-password + malformed cases.
Task 2: GREEN — implement parseCert + wire POST /parse
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 (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)
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).
pnpm --filter @tessera/api test cert-manager --run
- `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
parseCert returns full CertDetails for PEM/DER/PFX/P7B, throws 400 on wrong password/malformed input, and POST /parse is wired; API tests green.
Task 3: Inspect tab UI + action + render test
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
- 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)
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.
pnpm --filter @tessera/web test cert-manager --run
- `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
Inspect tab loads a cert end-to-end, renders the details grid on success and a localized destructive error on failure; web tests green.
<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