fcac0a3bfd
- 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>
97 lines
3.8 KiB
TypeScript
97 lines
3.8 KiB
TypeScript
import { Body, Controller, HttpCode, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
|
|
import { FilesInterceptor } from '@nestjs/platform-express';
|
|
import type { UploadedFileLike } from '../auth/types/auth-user';
|
|
import { UseModule } from '../module-registry/module.guard';
|
|
import { analyzeWorkingSet } from './cert-analyze';
|
|
import { buildOutput } from './cert-output';
|
|
import { type AnalysisResult, type BuildResult, certError } from './cert-types';
|
|
import { BuildOutputDto } from './dto/cert-build.dto';
|
|
|
|
/** Obergrenzen (D-17): je Datei 5 MiB, alle Dateien zusammen 20 MiB, hoechstens 30 Dateien. */
|
|
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.
|
|
*/
|
|
export function repairFileName(name: string): string {
|
|
const utf8 = Buffer.from(name, 'latin1').toString('utf8');
|
|
return utf8.includes('�') ? name : utf8;
|
|
}
|
|
|
|
/**
|
|
* CertManagerController: Zertifikat-Manager (quick-261009-ikt, D-14).
|
|
*
|
|
* Zustandslos: nichts wird gespeichert, Passwoerter und Schluessel kommen nie in ein Log
|
|
* (kein Logger-Aufruf mit Anfrageinhalt; das Request-Log kennt nur Pfad und Status).
|
|
* Geschuetzt durch den globalen JwtAuthGuard, den TenantGuard und @UseModule('cert-manager').
|
|
*
|
|
* Routen (alle POST, 200):
|
|
* - 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
|
|
*/
|
|
@Controller('modules/cert-manager')
|
|
@UseModule('cert-manager')
|
|
export class CertManagerController {
|
|
@Post('analyze')
|
|
@HttpCode(200)
|
|
@UseInterceptors(
|
|
FilesInterceptor('files', CERT_MAX_FILES, { limits: { fileSize: CERT_MAX_FILE_BYTES } }),
|
|
)
|
|
analyze(
|
|
@UploadedFiles() files: UploadedFileLike[] | undefined,
|
|
@Body('passwords') rawPasswords?: unknown,
|
|
): AnalysisResult {
|
|
if (!files || files.length === 0) {
|
|
certError('invalidInput', 400, 'No files provided');
|
|
}
|
|
const total = files.reduce((sum, f) => sum + f.buffer.length, 0);
|
|
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,
|
|
);
|
|
}
|
|
|
|
@Post('build')
|
|
@HttpCode(200)
|
|
build(@Body() dto: BuildOutputDto): BuildResult {
|
|
return buildOutput(dto);
|
|
}
|
|
}
|