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
+74 -2
View File
@@ -1,9 +1,9 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { buildChains } from './cert-chain';
import { buildChains, matchKeys } from './cert-chain';
import { detectBlob } from './cert-model';
import type { CertItem } from './cert-types';
import type { AnyItem, CertItem, CsrItem, KeyItem } from './cert-types';
const fx = (name: string) => readFileSync(join(__dirname, '__fixtures__', name));
@@ -150,3 +150,75 @@ describe('buildChains', () => {
expect(buildChains([])).toEqual({ chains: [] });
});
});
describe('matchKeys', () => {
const PASSWORD = 'Test-Pass-123';
function all(...names: [string, string?][]): AnyItem[] {
const items: AnyItem[] = [];
names.forEach(([name, password], file) => {
const result = detectBlob(fx(name), {
file,
path: name,
passwords: password ? [password] : [],
ownPassword: password ?? '',
});
items.push(...result.items);
});
return items;
}
const certsOf = (items: AnyItem[]) =>
items.filter((i): i is CertItem => i.kind === 'certificate');
const keysOf = (items: AnyItem[]) => items.filter((i): i is KeyItem => i.kind === 'privateKey');
const csrsOf = (items: AnyItem[]) => items.filter((i): i is CsrItem => i.kind === 'csr');
it('ordnet Schluessel und Anfrage dem Serverzertifikat zu (RSA und EC)', () => {
const items = all(
['rsa-leaf.pem'],
['rsa-leaf-key-enc-trad.pem', PASSWORD],
['rsa-leaf.csr'],
['ec-leaf.pem'],
['ec-leaf-key-sec1.der'],
['ec-leaf.csr.der'],
);
matchKeys(certsOf(items), keysOf(items), csrsOf(items));
const certs = certsOf(items);
const rsa = certs.find((c) => c.cn === 'www.example.test') as CertItem;
const ec = certs.find((c) => c.cn === 'ec.example.test') as CertItem;
const rsaKey = keysOf(items).find((k) => k.keyType === 'RSA') as KeyItem;
const ecKey = keysOf(items).find((k) => k.keyType === 'EC') as KeyItem;
const rsaCsr = csrsOf(items).find((r) => r.keyType === 'RSA') as CsrItem;
const ecCsr = csrsOf(items).find((r) => r.keyType === 'EC') as CsrItem;
expect(rsa.keyId).toBe(rsaKey.id);
expect(rsaKey.certIds).toEqual([rsa.id]);
expect(rsa.csrIds).toEqual([rsaCsr.id]);
expect(rsaCsr.keyId).toBe(rsaKey.id);
expect(rsaCsr.certIds).toEqual([rsa.id]);
expect(ec.keyId).toBe(ecKey.id);
expect(ecKey.certIds).toEqual([ec.id]);
expect(ecCsr.keyId).toBe(ecKey.id);
expect(ecCsr.certIds).toEqual([ec.id]);
});
it('ein fremder Schluessel gehoert zu keinem Zertifikat', () => {
const items = all(['rsa-leaf.pem'], ['ec-leaf-key.pem']);
matchKeys(certsOf(items), keysOf(items), csrsOf(items));
expect(certsOf(items)[0].keyId).toBeNull();
expect(keysOf(items)[0].certIds).toEqual([]);
});
it('Anfrage ohne Zertifikat und ohne Schluessel bleibt ohne Zuordnung', () => {
const items = all(['ec-leaf.csr']);
matchKeys(certsOf(items), keysOf(items), csrsOf(items));
expect(csrsOf(items)[0].keyId).toBeNull();
expect(csrsOf(items)[0].certIds).toEqual([]);
});
it('Zwischenzertifikat und Wurzel bekommen nie einen Schluessel, die Zuordnung ist wiederholbar', () => {
const items = all(['rsa-leaf.pem'], ['rsa-inter.pem'], ['rsa-leaf-key.pem']);
matchKeys(certsOf(items), keysOf(items), csrsOf(items));
matchKeys(certsOf(items), keysOf(items), csrsOf(items));
const withKey = certsOf(items).filter((c) => c.keyId !== null);
expect(withKey.map((c) => c.cn)).toEqual(['www.example.test']);
expect(keysOf(items)[0].certIds).toHaveLength(1);
});
});