feat(cert-manager): Zusammenführen mit Fullchain und Nur Kette
- Kettenbau mit Aussteller- und Signaturprüfung (checkIssued plus verify), Wurzel nur auf Wunsch - build-Route für Fullchain und Nur Kette (PEM), eigene JSON-Grenze 512 KiB mit 413 und Code tooLarge - Reiter Zusammenführen mit Kettenansicht, Hinweis bei fehlendem Zwischenzertifikat Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,82 @@
|
||||
import { X509Certificate } from 'node:crypto';
|
||||
import { buildChains } from './cert-chain';
|
||||
import { certItemFromDer } from './cert-model';
|
||||
import { safeBaseName } from './cert-names';
|
||||
import {
|
||||
type BuildFile,
|
||||
type BuildInput,
|
||||
type BuildResult,
|
||||
type CertItem,
|
||||
certError,
|
||||
} from './cert-types';
|
||||
|
||||
/**
|
||||
* Ausgabe-Bausteine des Zertifikat-Managers (quick-261009-ikt).
|
||||
* Task 1: nur der Dateiname-Helfer; die Ausgabeformate (build) kommen in den folgenden Tasks.
|
||||
* Ausgabe-Bausteine des Zertifikat-Managers (quick-261009-ikt, D-19).
|
||||
* Task 2: Fullchain und Nur Kette als PEM. Die weiteren Inhalte und Formate (Task 5) kommen in
|
||||
* dieselbe Funktion. Die Reihenfolge baut die API immer selbst aus den gesendeten Zertifikaten
|
||||
* (buildChains); eine vom Browser mitgeschickte Reihenfolge wird nie uebernommen.
|
||||
*/
|
||||
|
||||
/** Dateiname ohne Pfad und ohne gefaehrliche Zeichen, z. B. „*.example.de“ -> „wildcard.example.de“. */
|
||||
export function safeBaseName(raw: string, fallback: string): string {
|
||||
const cleaned = raw
|
||||
.replace(/^\*\./, 'wildcard.')
|
||||
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
||||
.replace(/^[._]+/, '')
|
||||
.slice(0, 80);
|
||||
return cleaned || fallback;
|
||||
export { safeBaseName };
|
||||
|
||||
const PEM_MIME = 'application/x-pem-file';
|
||||
|
||||
function parseCertificate(pem: unknown): CertItem {
|
||||
if (typeof pem !== 'string' || pem.trim() === '') {
|
||||
certError('notACertificate', 400, 'Value is not a certificate');
|
||||
}
|
||||
try {
|
||||
return certItemFromDer(new X509Certificate(pem).raw, { file: 0, path: '' });
|
||||
} catch {
|
||||
return certError('notACertificate', 400, 'Value is not a certificate');
|
||||
}
|
||||
}
|
||||
|
||||
/** PEM-Bloecke in Kettenreihenfolge, jeder genau einmal mit abschliessendem Zeilenumbruch. */
|
||||
function joinPem(certs: CertItem[]): string {
|
||||
return certs.map((c) => `${c.pem.trim()}\n`).join('');
|
||||
}
|
||||
|
||||
function pemFile(filename: string, certs: CertItem[]): BuildFile {
|
||||
return {
|
||||
filename,
|
||||
content: Buffer.from(joinPem(certs), 'utf8').toString('base64'),
|
||||
mimeType: PEM_MIME,
|
||||
};
|
||||
}
|
||||
|
||||
/** Baut die gewuenschte Ausgabe. Wirft Nest-Ausnahmen mit Code (D-24); nie mit Passwort oder Schluessel im Text. */
|
||||
export function buildOutput(input: BuildInput): BuildResult {
|
||||
if (!input.certPem) certError('invalidInput', 400, 'certPem is required');
|
||||
const head = parseCertificate(input.certPem);
|
||||
const pool = (input.poolPems ?? []).map(parseCertificate);
|
||||
|
||||
const unique = new Map<string, CertItem>();
|
||||
for (const c of [head, ...pool]) if (!unique.has(c.id)) unique.set(c.id, c);
|
||||
const all = [...unique.values()];
|
||||
|
||||
const chain = buildChains(all, [head.id]).chains[0];
|
||||
const byId = new Map(all.map((c) => [c.id, c]));
|
||||
const path = chain.path.map((id) => byId.get(id) as CertItem);
|
||||
const includeRoot = input.includeRoot === true;
|
||||
const withoutRoot = path.filter((c) => !(c.role === 'root' && c.id !== head.id));
|
||||
const base = safeBaseName(input.baseName ?? '', head.baseName);
|
||||
|
||||
let files: BuildFile[];
|
||||
if (input.content === 'fullchain') {
|
||||
files = [pemFile(`${base}-fullchain.pem`, includeRoot ? path : withoutRoot)];
|
||||
} else if (input.content === 'chain') {
|
||||
const issuers = (includeRoot ? path : withoutRoot).filter((c) => c.id !== head.id);
|
||||
if (issuers.length === 0)
|
||||
certError('noChain', 400, 'No intermediate or root certificate available');
|
||||
files = [pemFile(`${base}-chain.pem`, issuers)];
|
||||
} else {
|
||||
return certError('formatNotPossible', 400, 'Output not available');
|
||||
}
|
||||
|
||||
return {
|
||||
files,
|
||||
chainComplete: chain.complete,
|
||||
missingIssuerCn: chain.gap?.missingIssuerCn ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user