feat(cert-manager): Vorlagen für Zielsysteme, Modulversion 1.2.0 und Anleitungen

- Sieben Vorlagen (Nginx, Apache ab/vor 2.4.8, Windows/IIS, Nginx Proxy Manager, HAProxy, Tomcat) mit Dateien und Einrichtungszeilen
- Reiter „Vorlagen“ mit ZIP samt Anleitung, Schnipsel und Kopieren
- Modulversion 1.2.0, Modul-Changelog, CHANGELOG und drei Anleitungen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-09 15:57:48 +02:00
parent 65a1dca80f
commit 47b26219b2
19 changed files with 1434 additions and 14 deletions
@@ -6,6 +6,37 @@ import type { ModuleChangelog } from '../module-registry/module-changelog';
* docs/anleitung-entwicklung.md, Abschnitt „Modulversion und Modul-Changelog pflegen“.
*/
export const CERT_MANAGER_CHANGELOG: ModuleChangelog = [
{
version: '1.2.0',
date: '2026-10-09',
changes: [
{
kind: 'new',
de: 'Ein gemeinsamer Reiter „Dateien“: Laden Sie mehrere Dateien und ZIP-Dateien auf einmal hoch oder fügen Sie PEM-Text ein. Alle anderen Reiter arbeiten mit dieser Liste.',
en: 'One shared “Files” tab: upload several files and ZIP files at once or paste PEM text. All other tabs work with this list.',
},
{
kind: 'new',
de: '„Zusammenführen“ ordnet die Kette selbst und liefert Fullchain, nur die Kette, Zertifikat mit Schlüssel oder eine PFX-Datei; das Root-Zertifikat nehmen Sie nur auf Wunsch mit.',
en: '“Merge” orders the chain by itself and delivers a full chain, the chain only, certificate with key or a PFX file; the root certificate is only included on request.',
},
{
kind: 'new',
de: 'Alle gängigen Formate rein und raus, auch Zertifikate und Schlüssel mit elliptischen Kurven (EC) und verschlüsselte Schlüssel. PFX-Dateien wahlweise kompatibel oder modern verschlüsselt.',
en: 'All common formats in and out, including elliptic-curve (EC) certificates and keys and encrypted keys. PFX files with compatible or modern encryption.',
},
{
kind: 'new',
de: 'Vorlagen für Nginx, Apache, Windows/IIS, Nginx Proxy Manager, HAProxy und Tomcat liefern die passenden Dateien mit einem Klick.',
en: 'Templates for Nginx, Apache, Windows/IIS, Nginx Proxy Manager, HAProxy and Tomcat deliver the right files with one click.',
},
{
kind: 'fixed',
de: 'Beim Zusammenführen ersetzt eine zweite Datei nicht mehr die erste.',
en: 'When merging, a second file no longer replaces the first one.',
},
],
},
{
version: '1.1.0',
date: '2026-10-02',
@@ -530,3 +530,34 @@ describe('buildOutput: ungueltige Paare', () => {
).toEqual({ status: 400, code: 'invalidInput' });
});
});
describe('buildOutput: Vorlagen im Ergebnis', () => {
it('liefert bei Vorlagen einen Schnipsel, bei anderen Inhalten keinen', () => {
const tpl = buildOutput({
content: 'template',
template: 'haproxy',
certPem: fxText('rsa-leaf.pem'),
poolPems: [fxText('rsa-inter.pem')],
keyPem: fxText('rsa-leaf-key.pem'),
});
expect(tpl.snippet).toBe('bind :443 ssl crt /etc/haproxy/certs/www.example.test.pem');
expect(tpl.chainComplete).toBe(false);
expect(tpl.missingIssuerCn).toBe('Tessera Test Root RSA');
const plain = buildOutput({ content: 'leaf', certPem: fxText('rsa-leaf.pem') });
expect('snippet' in plain).toBe(false);
});
it('Vorlage mit Format: 400 invalidInput', () => {
expect(
codeOf(() =>
buildOutput({
content: 'template',
template: 'nginx',
format: 'pem',
certPem: fxText('rsa-leaf.pem'),
keyPem: fxText('rsa-leaf-key.pem'),
}),
),
).toEqual({ status: 400, code: 'invalidInput' });
});
});
+23
View File
@@ -6,6 +6,7 @@ import { exportKey, type KeyExportFormat } from './cert-keys';
import { certItemFromDer } from './cert-model';
import { safeBaseName } from './cert-names';
import { writePkcs12 } from './cert-pkcs12';
import { buildTemplate, isTemplateId } from './cert-templates';
import {
type BuildContent,
type BuildFile,
@@ -44,6 +45,7 @@ const FORMATS: Record<BuildContent, readonly string[]> = {
pfx: ['pfx'],
key: ['pkcs8', 'traditional', 'pkcs8-der'],
csr: ['pem', 'der'],
template: ['template'],
};
function parseCertificate(pem: unknown): CertItem {
@@ -172,6 +174,7 @@ export function buildOutput(input: BuildInput): BuildResult {
const base = safeBaseName(input.baseName ?? '', head.baseName);
let files: BuildFile[];
let snippet: string | null = null;
switch (input.content) {
case 'leaf':
if (format === 'der') files = [file(`${base}.cer`, derOf(head), MIME.der)];
@@ -208,6 +211,25 @@ export function buildOutput(input: BuildInput): BuildResult {
files = [file(`${base}.pfx`, der, MIME.pfx)];
break;
}
case 'template': {
if (!isTemplateId(input.template)) certError('invalidInput', 400, 'Unknown template');
// Eine Vorlage ohne passenden Schluessel gibt es nicht (templateNeedsKey); ein falscher Schluessel ist keyMismatch.
if (!input.keyPem || input.keyPem.trim() === '') {
certError('templateNeedsKey', 400, 'The template needs the matching private key');
}
const key = matchingKey(head, input.keyPem);
const built = buildTemplate(input.template, {
head,
shown,
key,
base,
password: input.password,
pfxProfile: input.pfxEncryption === 'modern' ? 'modern' : 'compat',
});
files = built.files;
snippet = built.snippet;
break;
}
default:
return certError('invalidInput', 400, 'Unknown content');
}
@@ -216,6 +238,7 @@ export function buildOutput(input: BuildInput): BuildResult {
files,
chainComplete: chain.complete,
missingIssuerCn: chain.gap?.missingIssuerCn ?? null,
...(input.content === 'template' ? { snippet } : {}),
};
}
@@ -0,0 +1,261 @@
import { createPrivateKey, X509Certificate } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import * as forge from 'node-forge';
import { describe, expect, it } from 'vitest';
import { buildOutput } from './cert-output';
import { readPkcs12 } from './cert-pkcs12';
import { TEMPLATE_IDS } from './cert-templates';
import type { BuildFile, BuildInput } from './cert-types';
const fxText = (name: string) => readFileSync(join(__dirname, '__fixtures__', name), 'utf8');
const bytes = (file: BuildFile) => Buffer.from(file.content, 'base64');
const text = (file: BuildFile) => bytes(file).toString('utf8');
const PASSWORD = 'Neu-Pass-2026';
const SETS = {
rsa: {
cn: 'www.example.test',
certPem: fxText('rsa-leaf.pem'),
poolPems: [fxText('rsa-root.pem'), fxText('rsa-inter.pem')],
keyPem: fxText('rsa-leaf-key.pem'),
traditionalHeader: '-----BEGIN RSA PRIVATE KEY-----',
},
ec: {
cn: 'ec.example.test',
certPem: fxText('ec-leaf.pem'),
poolPems: [fxText('ec-root.pem'), fxText('ec-inter.pem')],
keyPem: fxText('ec-leaf-key.pem'),
traditionalHeader: '-----BEGIN EC PRIVATE KEY-----',
},
} as const;
function codeOf(fn: () => unknown): { status: number; code: string } {
try {
fn();
} catch (error) {
const e = error as { getStatus(): number; getResponse(): { code: string } };
return { status: e.getStatus(), code: e.getResponse().code };
}
throw new Error('expected a throw');
}
function blocks(pem: string): string[] {
return pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g) ?? [];
}
function cnsOf(pem: string): string[] {
return blocks(pem).map((b) => String(new X509Certificate(b).toLegacyObject().subject.CN));
}
function names(files: BuildFile[]): string[] {
return files.map((f) => f.filename);
}
function template(id: string, set: keyof typeof SETS, extra: Partial<BuildInput> = {}) {
const s = SETS[set];
return buildOutput({
content: 'template',
template: id,
certPem: s.certPem,
poolPems: [...s.poolPems],
keyPem: s.keyPem,
...extra,
});
}
describe('Vorlagen: Kennungen', () => {
it('kennt die sieben Zielsysteme', () => {
expect([...TEMPLATE_IDS]).toEqual([
'nginx',
'apache',
'apache-legacy',
'iis',
'npm',
'haproxy',
'tomcat',
]);
});
});
describe.each(['rsa', 'ec'] as const)('Vorlagen (%s)', (set) => {
const s = SETS[set];
it('nginx: fullchain.pem (Server, Zwischen) und privkey.pem (PKCS#8) plus Schnipsel mit Basisnamen', () => {
const r = template('nginx', set);
expect(names(r.files)).toEqual(['fullchain.pem', 'privkey.pem']);
expect(cnsOf(text(r.files[0]))[0]).toBe(s.cn);
expect(cnsOf(text(r.files[0]))).toHaveLength(2);
expect(text(r.files[1])).toContain('-----BEGIN PRIVATE KEY-----');
expect(new X509Certificate(s.certPem).checkPrivateKey(createPrivateKey(text(r.files[1])))).toBe(
true,
);
expect(r.snippet).toBe(
`ssl_certificate /etc/nginx/ssl/${s.cn}/fullchain.pem;\nssl_certificate_key /etc/nginx/ssl/${s.cn}/privkey.pem;`,
);
expect(r.chainComplete).toBe(true);
});
it('nginx mit Wurzel: drei Zertifikate, Wurzel zuletzt', () => {
const r = template('nginx', set, { includeRoot: true });
expect(cnsOf(text(r.files[0]))).toHaveLength(3);
expect(cnsOf(text(r.files[0]))[2]).toContain('Root');
});
it('apache (ab 2.4.8): fullchain.pem und privkey.pem, SSLCertificateFile und SSLCertificateKeyFile', () => {
const r = template('apache', set);
expect(names(r.files)).toEqual(['fullchain.pem', 'privkey.pem']);
expect(r.snippet).toContain(`SSLCertificateFile /etc/ssl/${s.cn}/fullchain.pem`);
expect(r.snippet).toContain(`SSLCertificateKeyFile /etc/ssl/${s.cn}/privkey.pem`);
expect(r.snippet).not.toContain('SSLCertificateChainFile');
});
it('apache-legacy: cert.pem (nur Server), chain.pem (nur Aussteller), privkey.pem', () => {
const r = template('apache-legacy', set);
expect(names(r.files)).toEqual(['cert.pem', 'chain.pem', 'privkey.pem']);
expect(cnsOf(text(r.files[0]))).toEqual([s.cn]);
expect(cnsOf(text(r.files[1]))).toHaveLength(1);
expect(cnsOf(text(r.files[1]))[0]).toContain('Inter');
expect(r.snippet).toContain('SSLCertificateChainFile');
});
it('iis: <base>.pfx mit kompatibler Verschluesselung, mit dem Passwort lesbar, Schluessel passt', () => {
const r = template('iis', set, { password: PASSWORD });
expect(names(r.files)).toEqual([`${s.cn}.pfx`]);
const der = bytes(r.files[0]);
// 3DES-Schluesselbeutel (pbeWithSHA1And3-KeyTripleDES-CBC = 1.2.840.113549.1.12.1.3)
expect(der.includes(Buffer.from('2a864886f70d010c0103', 'hex'))).toBe(true);
const read = readPkcs12(der, [], PASSWORD);
expect(read.ok).toBe(true);
if (!read.ok) return;
expect(new X509Certificate(s.certPem).checkPrivateKey(read.contents.keys[0].key)).toBe(true);
expect(r.snippet).toBe(
`Import-PfxCertificate -FilePath .\\${s.cn}.pfx -CertStoreLocation Cert:\\LocalMachine\\My -Password (Read-Host -AsSecureString)`,
);
expect(r.snippet).not.toContain(PASSWORD);
});
it('npm: certificate.pem nur Server, intermediate.pem nur Zwischen, privkey.pem klassisch, kein Schnipsel', () => {
const r = template('npm', set);
expect(names(r.files)).toEqual(['certificate.pem', 'intermediate.pem', 'privkey.pem']);
expect(cnsOf(text(r.files[0]))).toEqual([s.cn]);
expect(cnsOf(text(r.files[1]))).toHaveLength(1);
expect(cnsOf(text(r.files[1]))[0]).toContain('Inter');
expect(text(r.files[2]).startsWith(s.traditionalHeader)).toBe(true);
expect(r.snippet).toBeNull();
const withRoot = template('npm', set, { includeRoot: true });
expect(cnsOf(text(withRoot.files[1]))).toHaveLength(2);
});
it('haproxy: eine Datei mit Server, Zwischen und Schluessel in dieser Reihenfolge', () => {
const r = template('haproxy', set);
expect(names(r.files)).toEqual([`${s.cn}.pem`]);
const pem = text(r.files[0]);
expect(cnsOf(pem)).toHaveLength(2);
expect(cnsOf(pem)[0]).toBe(s.cn);
expect(pem.indexOf('-----BEGIN PRIVATE KEY-----')).toBeGreaterThan(
pem.lastIndexOf('-----END CERTIFICATE-----'),
);
expect(r.snippet).toBe(`bind :443 ssl crt /etc/haproxy/certs/${s.cn}.pem`);
});
it('tomcat: <base>.p12, Anzeigename gleich Basisname, Schnipsel mit IHR-PASSWORT ohne das echte Passwort', () => {
const r = template('tomcat', set, { password: PASSWORD });
expect(names(r.files)).toEqual([`${s.cn}.p12`]);
const read = readPkcs12(bytes(r.files[0]), [], PASSWORD);
expect(read.ok).toBe(true);
// friendlyName steckt verschluesselt im Container: mit forge und dem Passwort auslesen
const p12 = forge.pkcs12.pkcs12FromAsn1(
forge.asn1.fromDer(forge.util.createBuffer(bytes(r.files[0]).toString('binary'))),
PASSWORD,
);
const aliases = p12.safeContents.flatMap((c) =>
c.safeBags.flatMap((b) => (b.attributes?.friendlyName ?? []) as string[]),
);
// forge liefert den BMPString roh (UTF-16, big endian)
const decoded = aliases.map((a) => Buffer.from(a, 'binary').swap16().toString('utf16le'));
expect(decoded).toContain(s.cn);
expect(r.snippet).toContain('IHR-PASSWORT');
expect(r.snippet).toContain(`conf/${s.cn}.p12`);
expect(r.snippet).toContain(`certificateKeyAlias="${s.cn}"`);
expect(r.snippet).not.toContain(PASSWORD);
});
it('iis mit Modern: AES-256 im Schluesselbeutel (gewaehlt, nicht Vorgabe)', () => {
const r = template('iis', set, { password: PASSWORD, pfxEncryption: 'modern' });
const read = readPkcs12(bytes(r.files[0]), [], PASSWORD);
expect(read.ok).toBe(true);
// PBES2 = 1.2.840.113549.1.5.13
expect(bytes(r.files[0]).includes(Buffer.from('2a864886f70d01050d', 'hex'))).toBe(true);
});
});
describe('Vorlagen: Fehler', () => {
it('ohne Schluessel: 400 templateNeedsKey fuer jede Vorlage', () => {
for (const id of TEMPLATE_IDS) {
expect(
codeOf(() =>
buildOutput({
content: 'template',
template: id,
certPem: SETS.rsa.certPem,
password: PASSWORD,
}),
),
).toEqual({ status: 400, code: 'templateNeedsKey' });
}
});
it('falscher Schluessel: 400 keyMismatch', () => {
expect(codeOf(() => template('nginx', 'rsa', { keyPem: SETS.ec.keyPem }))).toEqual({
status: 400,
code: 'keyMismatch',
});
});
it('iis und tomcat ohne Passwort: 400 passwordRequired', () => {
for (const id of ['iis', 'tomcat']) {
expect(codeOf(() => template(id, 'rsa'))).toEqual({ status: 400, code: 'passwordRequired' });
expect(codeOf(() => template(id, 'rsa', { password: '' }))).toEqual({
status: 400,
code: 'passwordRequired',
});
}
});
it('unbekannte oder fehlende Kennung: 400 invalidInput', () => {
expect(codeOf(() => template('weblogic', 'rsa'))).toEqual({
status: 400,
code: 'invalidInput',
});
expect(
codeOf(() =>
buildOutput({
content: 'template',
certPem: SETS.rsa.certPem,
keyPem: SETS.rsa.keyPem,
}),
),
).toEqual({ status: 400, code: 'invalidInput' });
});
it('Kennung ohne Zertifikat: 400 invalidInput', () => {
expect(codeOf(() => buildOutput({ content: 'template', template: 'nginx' }))).toEqual({
status: 400,
code: 'invalidInput',
});
});
it('Antwort und Fehler enthalten weder Passwort noch Schluesseltext', () => {
const r = template('tomcat', 'rsa', { password: PASSWORD });
expect(JSON.stringify(r.snippet)).not.toContain(PASSWORD);
let message = '';
try {
template('iis', 'rsa', { keyPem: 'kein Schluessel', password: PASSWORD });
} catch (error) {
message = JSON.stringify((error as { getResponse(): unknown }).getResponse());
}
expect(message).not.toContain(PASSWORD);
expect(message).not.toContain('kein Schluessel');
});
});
+160
View File
@@ -0,0 +1,160 @@
import type { KeyObject } from 'node:crypto';
import { X509Certificate } from 'node:crypto';
import { exportKey } from './cert-keys';
import { type Pkcs12Profile, writePkcs12 } from './cert-pkcs12';
import { type BuildFile, type CertItem, certError } from './cert-types';
/**
* Vorlagen fuer Zielsysteme (quick-261009-ikt, D-04, D-21). Eine Vorlage liefert die fertigen Dateien
* und einen Konfigurationsschnipsel; mehrere Dateien packt der Browser zu einer ZIP-Datei (fflate).
* Alle Vorlagen brauchen das Serverzertifikat und den passenden privaten Schluessel (der Aufrufer
* hat beides schon geprueft). Der Schluessel liegt in den Dateien unverschluesselt, ausser in PFX-
* Dateien (Passwort). Die Wurzel kommt nur mit, wenn `shown` sie enthaelt (Haken „Root-Zertifikat
* mitnehmen“). Das Passwort erscheint nie in einem Schnipsel.
*
* Diese Datei kennt cert-output.ts nicht (sonst entstuende eine Importschleife); die paar Hilfen
* fuer Dateien und PEM-Bloecke stehen deshalb hier noch einmal in Kurzform.
*/
export const TEMPLATE_IDS = [
'nginx',
'apache',
'apache-legacy',
'iis',
'npm',
'haproxy',
'tomcat',
] as const;
export type TemplateId = (typeof TEMPLATE_IDS)[number];
export interface TemplateContext {
/** Serverzertifikat */
head: CertItem;
/** Kette in Reihenfolge: Serverzertifikat zuerst, dann die Aussteller (Wurzel nur auf Wunsch) */
shown: CertItem[];
/** der zum Serverzertifikat gehoerende private Schluessel */
key: KeyObject;
/** sicherer Basisname fuer Dateinamen und Schnipsel */
base: string;
password?: string;
pfxProfile: Pkcs12Profile;
}
export interface TemplateResult {
files: BuildFile[];
snippet: string | null;
}
const MIME_PEM = 'application/x-pem-file';
const MIME_PFX = 'application/x-pkcs12';
export function isTemplateId(value: unknown): value is TemplateId {
return typeof value === 'string' && (TEMPLATE_IDS as readonly string[]).includes(value);
}
function file(filename: string, data: Buffer | string, mimeType: string): BuildFile {
const bytes = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
return { filename, content: bytes.toString('base64'), mimeType };
}
function joinPem(certs: CertItem[]): string {
return certs.map((c) => `${c.pem.trim()}\n`).join('');
}
function derOf(cert: CertItem): Buffer {
return new X509Certificate(cert.pem).raw;
}
function pkcs8Pem(key: KeyObject): string {
return exportKey(key, 'pkcs8').toString('utf8');
}
function requirePassword(ctx: TemplateContext): string {
if (!ctx.password) certError('passwordRequired', 400, 'A password is required');
return ctx.password;
}
function pfxFile(ctx: TemplateContext, extension: string): BuildFile {
const der = writePkcs12({
keyObject: ctx.key,
certDers: ctx.shown.map(derOf),
password: requirePassword(ctx),
profile: ctx.pfxProfile,
friendlyName: ctx.base,
});
return file(`${ctx.base}.${extension}`, der, MIME_PFX);
}
/** Bauen je Vorlage. Wirft Nest-Ausnahmen mit Code; unbekannte Kennung ergibt invalidInput. */
export function buildTemplate(id: unknown, ctx: TemplateContext): TemplateResult {
if (!isTemplateId(id)) certError('invalidInput', 400, 'Unknown template');
const { base } = ctx;
const issuers = ctx.shown.filter((c) => c.id !== ctx.head.id);
switch (id) {
case 'nginx':
return {
files: [
file('fullchain.pem', joinPem(ctx.shown), MIME_PEM),
file('privkey.pem', pkcs8Pem(ctx.key), MIME_PEM),
],
snippet: [
`ssl_certificate /etc/nginx/ssl/${base}/fullchain.pem;`,
`ssl_certificate_key /etc/nginx/ssl/${base}/privkey.pem;`,
].join('\n'),
};
case 'apache':
return {
files: [
file('fullchain.pem', joinPem(ctx.shown), MIME_PEM),
file('privkey.pem', pkcs8Pem(ctx.key), MIME_PEM),
],
snippet: [
`SSLCertificateFile /etc/ssl/${base}/fullchain.pem`,
`SSLCertificateKeyFile /etc/ssl/${base}/privkey.pem`,
].join('\n'),
};
case 'apache-legacy': {
// Ohne Aussteller gibt es keine Kettendatei; dann entfallen Datei und Zeile.
const files = [file('cert.pem', joinPem([ctx.head]), MIME_PEM)];
const lines = [
`SSLCertificateFile /etc/ssl/${base}/cert.pem`,
`SSLCertificateKeyFile /etc/ssl/${base}/privkey.pem`,
];
if (issuers.length > 0) {
files.push(file('chain.pem', joinPem(issuers), MIME_PEM));
lines.push(`SSLCertificateChainFile /etc/ssl/${base}/chain.pem`);
}
files.push(file('privkey.pem', pkcs8Pem(ctx.key), MIME_PEM));
return { files, snippet: lines.join('\n') };
}
case 'iis':
return {
files: [pfxFile(ctx, 'pfx')],
snippet: `Import-PfxCertificate -FilePath .\\${base}.pfx -CertStoreLocation Cert:\\LocalMachine\\My -Password (Read-Host -AsSecureString)`,
};
case 'npm': {
// Nginx Proxy Manager verlangt bei RSA oft „RSA PRIVATE KEY“ (PKCS#1), bei EC „EC PRIVATE KEY“ (SEC1).
let keyPem: string;
try {
keyPem = exportKey(ctx.key, 'traditional').toString('utf8');
} catch {
keyPem = pkcs8Pem(ctx.key);
}
const files = [file('certificate.pem', joinPem([ctx.head]), MIME_PEM)];
if (issuers.length > 0) files.push(file('intermediate.pem', joinPem(issuers), MIME_PEM));
files.push(file('privkey.pem', keyPem, MIME_PEM));
return { files, snippet: null };
}
case 'haproxy':
return {
files: [file(`${base}.pem`, joinPem(ctx.shown) + pkcs8Pem(ctx.key), MIME_PEM)],
snippet: `bind :443 ssl crt /etc/haproxy/certs/${base}.pem`,
};
case 'tomcat':
return {
files: [pfxFile(ctx, 'p12')],
snippet: `<Certificate certificateKeystoreFile="conf/${base}.p12" certificateKeystorePassword="IHR-PASSWORT" certificateKeystoreType="PKCS12" certificateKeyAlias="${base}" />`,
};
}
}
+9 -1
View File
@@ -129,7 +129,15 @@ export interface AnalysisResult {
ignored: IgnoredEntry[];
}
export type BuildContent = 'leaf' | 'fullchain' | 'chain' | 'leafKey' | 'pfx' | 'key' | 'csr';
export type BuildContent =
| 'leaf'
| 'fullchain'
| 'chain'
| 'leafKey'
| 'pfx'
| 'key'
| 'csr'
| 'template';
export interface BuildInput {
content: BuildContent;
@@ -7,6 +7,7 @@ import {
IsString,
MaxLength,
} from 'class-validator';
import { TEMPLATE_IDS } from '../cert-templates';
/**
* Anfrage fuer POST build (quick-261009-ikt, D-19). Die Obergrenzen stehen hier als Konstanten,
@@ -16,7 +17,7 @@ import {
* + password 256 + baseName 120 + JSON-Maskierung der Zeilenumbrueche (etwa +1,6 %)
* = rund 384 kB, also deutlich unter dem Grenzwert von 512 KiB.
*
* Stand Task 5: alle Inhalte und Formate aus D-19; die Vorlage (template) folgt in Task 6.
* Stand Task 6: alle Inhalte und Formate aus D-19 sowie die Vorlage (template, nur eine Kennung).
*/
export const CERT_PEM_MAX = 16_384;
export const CERT_POOL_MAX = 20;
@@ -31,6 +32,7 @@ export const BUILD_CONTENTS = [
'pfx',
'key',
'csr',
'template',
] as const;
export const BUILD_FORMATS = [
'pem',
@@ -91,6 +93,10 @@ export class BuildOutputDto {
@IsIn(BUILD_PFX_ENCRYPTIONS)
pfxEncryption?: (typeof BUILD_PFX_ENCRYPTIONS)[number];
@IsOptional()
@IsIn(TEMPLATE_IDS)
template?: (typeof TEMPLATE_IDS)[number];
@IsOptional()
@IsString()
@MaxLength(CERT_BASENAME_MAX)