feat(cert-manager): Schlüssel, PFX und CSR erkennen, Passwort je Datei

- Private Schlüssel (PKCS#1, PKCS#8, SEC1; PEM und DER; unverschlüsselt, verschlüsselt, klassisch verschlüsselt) für RSA und EC über node:crypto
- PKCS#12 lesen (OpenSSL 3, kompatibel, RC2; EC-Zertifikate und -Schlüssel), auch ohne Endung und im ZIP
- Zertifikatsanfragen (CSR) als PEM und DER mit Inhaber, SAN und Schlüssel
- Zuordnung von Schlüssel und Anfrage zum Zertifikat (checkPrivateKey, SPKI-Vergleich)
- Feld passwords je Datei, gesperrte Dateien fragen nach dem Passwort; kein Passwort in Antwort oder Log
- Oberfläche: Passwortfeld mit Anzeigen/Verbergen, Schlüssel- und Anfragekarten im Reiter Analysieren

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-09 15:31:54 +02:00
parent 1554ae83c1
commit fcac0a3bfd
28 changed files with 2140 additions and 100 deletions
@@ -12,6 +12,35 @@ export const CERT_MAX_FILES = 30;
export const CERT_MAX_FILE_BYTES = 5 * 1024 * 1024;
export const CERT_MAX_TOTAL_BYTES = 20 * 1024 * 1024;
/** Das Feld `passwords`: ein JSON-Array von Zeichenketten, je Datei eines (Task 4). */
export const CERT_MAX_PASSWORDS = 30;
export const CERT_MAX_PASSWORD_CHARS = 1024;
/**
* Liest das multipart-Feld `passwords`. Fehlt es, gibt es keine Passwoerter. Alles andere als ein
* JSON-Array aus hoechstens 30 Zeichenketten zu je hoechstens 1024 Zeichen: 400 invalidInput.
* Der Inhalt erscheint nie in einer Fehlermeldung.
*/
export function parsePasswords(raw: unknown): string[] {
if (raw === undefined || raw === null || raw === '') return [];
let value: unknown = raw;
if (typeof raw === 'string') {
try {
value = JSON.parse(raw);
} catch {
certError('invalidInput', 400, 'passwords must be a JSON array of strings');
}
}
if (
!Array.isArray(value) ||
value.length > CERT_MAX_PASSWORDS ||
value.some((p) => typeof p !== 'string' || p.length > CERT_MAX_PASSWORD_CHARS)
) {
certError('invalidInput', 400, 'passwords must be a JSON array of strings');
}
return value as string[];
}
/**
* multer liest Dateinamen als Latin-1. Waren es UTF-8-Bytes (Umlaute), den Namen zurueckgewinnen;
* ist das Ergebnis kein gueltiges UTF-8, bleibt der Name wie er ist.
@@ -29,7 +58,7 @@ export function repairFileName(name: string): string {
* Geschuetzt durch den globalen JwtAuthGuard, den TenantGuard und @UseModule('cert-manager').
*
* Routen (alle POST, 200):
* - analyze Task 1 mehrere Dateien (multipart) erkennen und zusammenfassen
* - analyze Task 1 mehrere Dateien (multipart) erkennen und zusammenfassen; Task 4: optionales Feld `passwords`
* - build Task 2 Ausgabe bauen (JSON, eigene Grenze 512 KiB, siehe cert-json-body.ts), ab Task 5/6 erweitert
* - fetch-issuer Task 7 fehlendes Zwischenzertifikat nur auf Knopfdruck holen
*/
@@ -41,7 +70,10 @@ export class CertManagerController {
@UseInterceptors(
FilesInterceptor('files', CERT_MAX_FILES, { limits: { fileSize: CERT_MAX_FILE_BYTES } }),
)
analyze(@UploadedFiles() files: UploadedFileLike[] | undefined): AnalysisResult {
analyze(
@UploadedFiles() files: UploadedFileLike[] | undefined,
@Body('passwords') rawPasswords?: unknown,
): AnalysisResult {
if (!files || files.length === 0) {
certError('invalidInput', 400, 'No files provided');
}
@@ -49,8 +81,10 @@ export class CertManagerController {
if (total > CERT_MAX_TOTAL_BYTES) {
certError('tooLarge', 413, 'Files together exceed 20 MiB');
}
const passwords = parsePasswords(rawPasswords);
return analyzeWorkingSet(
files.map((f) => ({ originalname: repairFileName(f.originalname), buffer: f.buffer })),
passwords,
);
}