diff --git a/apps/api/src/cert-manager/cert-manager.controller.ts b/apps/api/src/cert-manager/cert-manager.controller.ts index 82a862e..dcb97d9 100644 --- a/apps/api/src/cert-manager/cert-manager.controller.ts +++ b/apps/api/src/cert-manager/cert-manager.controller.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'; import { UseModule } from '../module-registry/module.guard'; +import type { UploadedFileLike } from '../auth/types/auth-user'; import { CertManagerService } from './cert-manager.service'; /** @@ -39,7 +40,7 @@ export class CertManagerController { }), ) async parseCert( - @UploadedFile() file: any, + @UploadedFile() file: UploadedFileLike | undefined, @Body('password') password?: string, @Body('pemText') pemText?: string, ) { @@ -60,7 +61,7 @@ export class CertManagerController { }), ) async splitCerts( - @UploadedFile() file: any, + @UploadedFile() file: UploadedFileLike | undefined, @Body('password') password?: string, ) { if (!file) { @@ -81,7 +82,7 @@ export class CertManagerController { }), ) async mergeCerts( - @UploadedFiles() files: any[], + @UploadedFiles() files: UploadedFileLike[], @Body('outputFormat') outputFormat: string, @Body('password') password?: string, ) { @@ -107,7 +108,7 @@ export class CertManagerController { }), ) async convertCert( - @UploadedFile() file: any, + @UploadedFile() file: UploadedFileLike | undefined, @Body('targetFormat') targetFormat: string, @Body('password') password?: string, @Body('pemText') pemText?: string, diff --git a/apps/api/src/cert-manager/cert-manager.service.ts b/apps/api/src/cert-manager/cert-manager.service.ts index ab65b4b..c7de9cc 100644 --- a/apps/api/src/cert-manager/cert-manager.service.ts +++ b/apps/api/src/cert-manager/cert-manager.service.ts @@ -1,5 +1,15 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import * as forge from 'node-forge'; +import type { UploadedFileLike } from '../auth/types/auth-user'; + +/** + * Eine hochgeladene Zertifikatsdatei, so weit dieser Dienst sie liest: + * Inhalt und eingereichter Name (der Name geht ausschliesslich in + * `detectFormat` und in Fehlermeldungen). Abgeleitet aus `UploadedFileLike` + * statt daneben erfunden (quick-260921-m34); `mimetype` und `size` bleiben + * bewusst draussen, weil kein Zweig dieses Dienstes sie liest. + */ +type CertFileLike = Pick; // --------------------------------------------------------------------------- // CertDetails — the structured result returned by parseCert @@ -162,7 +172,7 @@ export class CertManagerService { * - Wrong PFX password → generic 400 message (password value never logged or echoed) */ async parseCert(input: { - file?: any; + file?: CertFileLike; pemText?: string; password?: string; }): Promise { @@ -179,11 +189,11 @@ export class CertManagerService { } cert = certs[0]; } else if (file) { - const format = this.detectFormat(file.originalname as string, file.buffer as Buffer); + const format = this.detectFormat(file.originalname, file.buffer); if (format === 'pem') { // ── PEM file ─────────────────────────────────────────────────────── - const pemStr = (file.buffer as Buffer).toString('utf-8'); + const pemStr = file.buffer.toString('utf-8'); const certs = this.parsePemChain(pemStr); if (certs.length === 0) { throw new Error('No certificate block found in PEM file'); @@ -192,12 +202,12 @@ export class CertManagerService { } else if (format === 'der') { // ── DER binary file ──────────────────────────────────────────────── // CRITICAL: binary encoding, never utf-8 (RESEARCH Pitfall 1) - const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); cert = forge.pki.certificateFromAsn1(asn1); } else if (format === 'pfx') { // ── PFX/PKCS12 file ─────────────────────────────────────────────── // wrong password → forge throws → caught below → BadRequestException (T-09-02) - const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? ''); const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); const bags = certBags[forge.pki.oids.certBag] ?? []; @@ -217,15 +227,15 @@ export class CertManagerService { cert = parsedCert; } else { // ── P7B/PKCS7 file — PEM-wrapped or binary DER (Pitfall 4) ──────── - const isPemP7b = (file.buffer as Buffer) + const isPemP7b = file.buffer .slice(0, 27) .toString('ascii') .includes('-----BEGIN'); let p7: any; if (isPemP7b) { - p7 = forge.pkcs7.messageFromPem((file.buffer as Buffer).toString('utf-8')); + p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8')); } else { - const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); p7 = forge.pkcs7.messageFromAsn1(p7Asn1); } const p7Certs: forge.pki.Certificate[] = p7.certificates ?? []; @@ -332,7 +342,7 @@ export class CertManagerService { * - File size limit 5 MB enforced by FileInterceptor in the controller */ async splitCerts(input: { - file?: any; + file?: CertFileLike; password?: string; }): Promise { const { file } = input; @@ -344,28 +354,28 @@ export class CertManagerService { let certs: forge.pki.Certificate[]; try { - const format = this.detectFormat(file.originalname as string, file.buffer as Buffer); + const format = this.detectFormat(file.originalname, file.buffer); if (format === 'pem') { // ── PEM chain (fullchain.pem, .crt — both map to 'pem' in detectFormat) ─ - const pemStr = (file.buffer as Buffer).toString('utf-8'); + const pemStr = file.buffer.toString('utf-8'); certs = this.parsePemChain(pemStr); if (certs.length === 0) { throw new Error('No certificate blocks found in PEM file'); } } else if (format === 'p7b') { // ── P7B/PKCS7 bundle — PEM-wrapped or binary DER (Pitfall 4) ───────── - const isPemP7b = (file.buffer as Buffer) + const isPemP7b = file.buffer .slice(0, 27) .toString('ascii') .includes('-----BEGIN'); let p7: any; if (isPemP7b) { // PEM-wrapped PKCS7 (e.g. -----BEGIN PKCS7-----) - p7 = forge.pkcs7.messageFromPem((file.buffer as Buffer).toString('utf-8')); + p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8')); } else { // Binary DER PKCS7 - const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); p7 = forge.pkcs7.messageFromAsn1(p7Asn1); } certs = (p7.certificates as forge.pki.Certificate[]) ?? []; @@ -434,7 +444,7 @@ export class CertManagerService { * No fallback to lower-level certBag construction was needed. */ async mergeCerts(input: { - files?: any[]; + files?: CertFileLike[]; outputFormat: string; password?: string; }): Promise { @@ -453,26 +463,23 @@ export class CertManagerService { let certs: forge.pki.Certificate[]; try { - certs = (files as any[]).flatMap((file: any) => { - const format = this.detectFormat( - file.originalname as string, - file.buffer as Buffer, - ); + certs = files.flatMap((file) => { + const format = this.detectFormat(file.originalname, file.buffer); if (format === 'pem') { - const pemStr = (file.buffer as Buffer).toString('utf-8'); + const pemStr = file.buffer.toString('utf-8'); const parsed = this.parsePemChain(pemStr); if (parsed.length === 0) { - throw new Error(`No certificate block found in ${file.originalname as string}`); + throw new Error(`No certificate block found in ${file.originalname}`); } return parsed; } else if (format === 'der') { // CRITICAL: binary encoding, never utf-8 (RESEARCH Pitfall 1) - const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); return [forge.pki.certificateFromAsn1(asn1)]; } else if (format === 'pfx') { // Extract all certs from the PFX bag - const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? ''); const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); const bags = certBags[forge.pki.oids.certBag] ?? []; @@ -488,21 +495,21 @@ export class CertManagerService { const missingCertIndex = bagCerts.findIndex((c) => c === undefined || c === null); if (missingCertIndex !== -1) { throw new BadRequestException( - `Certificate bag in "${file.originalname as string}" does not contain a readable X.509 certificate`, + `Certificate bag in "${file.originalname}" does not contain a readable X.509 certificate`, ); } return bagCerts.filter((c): c is forge.pki.Certificate => c !== undefined && c !== null); } else { // P7B/PKCS7 — PEM-wrapped or binary DER (Pitfall 4) - const isPemP7b = (file.buffer as Buffer) + const isPemP7b = file.buffer .slice(0, 27) .toString('ascii') .includes('-----BEGIN'); let p7: any; if (isPemP7b) { - p7 = forge.pkcs7.messageFromPem((file.buffer as Buffer).toString('utf-8')); + p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8')); } else { - const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); p7 = forge.pkcs7.messageFromAsn1(p7Asn1); } return (p7.certificates as forge.pki.Certificate[]) ?? []; @@ -571,7 +578,7 @@ export class CertManagerService { * - Password is never passed to the logger (T-09-02) */ async convertCert(input: { - file?: any; + file?: CertFileLike; pemText?: string; targetFormat: string; password?: string; @@ -602,10 +609,10 @@ export class CertManagerService { } cert = certs[0]; } else if (file) { - const format = this.detectFormat(file.originalname as string, file.buffer as Buffer); + const format = this.detectFormat(file.originalname, file.buffer); if (format === 'pem') { - const pemStr = (file.buffer as Buffer).toString('utf-8'); + const pemStr = file.buffer.toString('utf-8'); const certs = this.parsePemChain(pemStr); if (certs.length === 0) { throw new Error('No certificate block found in PEM file'); @@ -613,11 +620,11 @@ export class CertManagerService { cert = certs[0]; } else if (format === 'der') { // CRITICAL: binary encoding, never utf-8 (RESEARCH Pitfall 1) - const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); cert = forge.pki.certificateFromAsn1(asn1); } else if (format === 'pfx') { // PFX/PKCS12 — extract first cert bag (wrong password → BadRequestException) - const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? ''); const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); const bags = certBags[forge.pki.oids.certBag] ?? []; @@ -637,15 +644,15 @@ export class CertManagerService { cert = parsedCert; } else { // P7B — extract first cert - const isPemP7b = (file.buffer as Buffer) + const isPemP7b = file.buffer .slice(0, 27) .toString('ascii') .includes('-----BEGIN'); let p7: any; if (isPemP7b) { - p7 = forge.pkcs7.messageFromPem((file.buffer as Buffer).toString('utf-8')); + p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8')); } else { - const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer)); + const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer)); p7 = forge.pkcs7.messageFromAsn1(p7Asn1); } const p7Certs: forge.pki.Certificate[] = p7.certificates ?? [];