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
+70 -2
View File
@@ -1,5 +1,7 @@
import { X509Certificate } from 'node:crypto';
import type { CertItem, ChainGap, ChainInfo } from './cert-types';
import { createPrivateKey, createPublicKey, X509Certificate } from 'node:crypto';
import { csrPublicKeyOf } from './cert-csr';
import { spkiDerOf } from './cert-keys';
import type { CertItem, ChainGap, ChainInfo, CsrItem, KeyItem } from './cert-types';
/**
* Kettenbau des Zertifikat-Managers (quick-261009-ikt, D-18). Reine Funktionen, kein Netz.
@@ -146,3 +148,69 @@ export function buildChains(certs: CertItem[], headIds?: string[]): { chains: Ch
const now = Date.now();
return { chains: heads.map((h) => chainOf(nodes, h, now)) };
}
/**
* Ordnet Schluessel und Zertifikatsanfragen ihren Zertifikaten zu (D-18) und traegt die Zuordnung
* in die Eintraege ein (`keyId`, `certIds`, `csrIds`); vorherige Zuordnungen werden ersetzt.
*
* - Schluessel und Zertifikat: `cert.checkPrivateKey(key)`.
* - Anfrage und Zertifikat oder Schluessel: der oeffentliche Schluessel (SPKI-DER) ist derselbe.
* Nie ueber Namen oder Modulus-Zeichenketten: das geht bei EC nicht und waere bei RSA unsauber.
*/
export function matchKeys(certs: CertItem[], keys: KeyItem[], csrs: CsrItem[]): void {
const certObjects = certs.map((item) => {
item.keyId = null;
item.csrIds = [];
try {
const x = new X509Certificate(item.pem);
return { item, x, spki: spkiDerOf(x.publicKey) };
} catch {
return { item, x: null, spki: null };
}
});
const keyObjects = keys.map((item) => {
item.certIds = [];
try {
const key = createPrivateKey(item.pem);
return { item, key, spki: spkiDerOf(createPublicKey(key)) };
} catch {
return { item, key: null, spki: null };
}
});
for (const c of certObjects) {
if (!c.x) continue;
for (const k of keyObjects) {
if (!k.key) continue;
let matches = false;
try {
matches = c.x.checkPrivateKey(k.key);
} catch {
// nicht pruefbar (unbekannter Schluesseltyp): kein Treffer
}
if (matches) {
c.item.keyId ??= k.item.id;
k.item.certIds.push(c.item.id);
}
}
}
for (const csr of csrs) {
csr.keyId = null;
csr.certIds = [];
let spki: Buffer;
try {
spki = csrPublicKeyOf(csr.pem).spki;
} catch {
continue;
}
const key = keyObjects.find((k) => k.spki?.equals(spki));
if (key) csr.keyId = key.item.id;
for (const c of certObjects) {
if (c.spki?.equals(spki)) {
csr.certIds.push(c.item.id);
c.item.csrIds.push(csr.id);
}
}
}
}