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:
@@ -0,0 +1,112 @@
|
||||
import { X509Certificate } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { candidatePasswords, keyIdOf } from './cert-keys';
|
||||
import { detectBlob } from './cert-model';
|
||||
import { isPkcs12Der, readPkcs12 } from './cert-pkcs12';
|
||||
import type { CertItem, KeyItem } from './cert-types';
|
||||
|
||||
const fx = (name: string) => readFileSync(join(__dirname, '__fixtures__', name));
|
||||
const PASSWORD = 'Test-Pass-123';
|
||||
|
||||
const sha = (name: string) => new X509Certificate(fx(name)).fingerprint256;
|
||||
|
||||
describe('readPkcs12', () => {
|
||||
const cases = [
|
||||
['rsa-modern.pfx', 'rsa', ['rsa-leaf.pem', 'rsa-inter.pem', 'rsa-root.pem']],
|
||||
['rsa-compat.pfx', 'rsa', ['rsa-leaf.pem', 'rsa-inter.pem', 'rsa-root.pem']],
|
||||
['rsa-legacy.pfx', 'rsa', ['rsa-leaf.pem', 'rsa-inter.pem', 'rsa-root.pem']],
|
||||
['ec-modern.pfx', 'ec', ['ec-leaf.pem', 'ec-inter.pem', 'ec-root.pem']],
|
||||
['ec-compat.pfx', 'ec', ['ec-leaf.pem', 'ec-inter.pem', 'ec-root.pem']],
|
||||
] as const;
|
||||
|
||||
it.each(
|
||||
cases,
|
||||
)('%s: Zertifikate (auch EC) und Schluessel mit dem Passwort', (name, type, certFiles) => {
|
||||
const result = readPkcs12(fx(name), [PASSWORD], PASSWORD);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
const fingerprints = result.contents.certDers.map((d) => new X509Certificate(d).fingerprint256);
|
||||
expect(fingerprints.sort()).toEqual(certFiles.map(sha).sort());
|
||||
expect(result.contents.keys).toHaveLength(1);
|
||||
expect(result.contents.keys[0].key.asymmetricKeyType).toBe(type);
|
||||
expect(result.contents.keys[0].shrouded).toBe(true);
|
||||
});
|
||||
|
||||
it('rsa-nopass.pfx oeffnet sich ohne Passwort (leeres Passwort)', () => {
|
||||
const result = readPkcs12(fx('rsa-nopass.pfx'), []);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) expect(result.contents.certDers).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('ohne Passwort: passwordNeeded, mit falschem: passwordWrong', () => {
|
||||
expect(readPkcs12(fx('rsa-modern.pfx'), [])).toEqual({ ok: false, reason: 'passwordNeeded' });
|
||||
expect(readPkcs12(fx('rsa-modern.pfx'), ['falsch'], 'falsch')).toEqual({
|
||||
ok: false,
|
||||
reason: 'passwordWrong',
|
||||
});
|
||||
});
|
||||
|
||||
it('probiert die uebrigen Passwoerter der Anfrage, wenn das eigene nicht passt', () => {
|
||||
const result = readPkcs12(fx('ec-compat.pfx'), ['falsch', 'noch-falsch', PASSWORD], 'falsch');
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('erkennt PKCS#12 am Aufbau, nicht an der Endung', () => {
|
||||
expect(isPkcs12Der(fx('rsa-modern.bin'))).toBe(true);
|
||||
expect(isPkcs12Der(fx('rsa-leaf.cer'))).toBe(false);
|
||||
expect(isPkcs12Der(Buffer.from('kein Container'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectBlob: PKCS#12', () => {
|
||||
function detect(name: string, own = '', others: string[] = []) {
|
||||
return detectBlob(fx(name), {
|
||||
file: 0,
|
||||
path: name,
|
||||
passwords: candidatePasswords(own, [own, ...others]),
|
||||
ownPassword: own,
|
||||
});
|
||||
}
|
||||
|
||||
it('ec-compat.pfx: EC-Zertifikate und EC-Schluessel werden Eintraege mit dem Container als Quelle', () => {
|
||||
const r = detect('ec-compat.pfx', PASSWORD);
|
||||
expect(r.locked).toEqual([]);
|
||||
const certs = r.items.filter((i): i is CertItem => i.kind === 'certificate');
|
||||
expect(certs.map((c) => c.cn).sort()).toEqual([
|
||||
'Tessera Test Inter EC',
|
||||
'Tessera Test Root EC',
|
||||
'ec.example.test',
|
||||
]);
|
||||
expect(certs.every((c) => c.keyType === 'EC')).toBe(true);
|
||||
expect(certs[0].sources).toEqual([{ file: 0, path: 'ec-compat.pfx' }]);
|
||||
const key = r.items.find((i): i is KeyItem => i.kind === 'privateKey');
|
||||
const leaf = new X509Certificate(fx('ec-leaf.pem'));
|
||||
expect(key?.id).toBe(keyIdOf(leaf.publicKey));
|
||||
expect(key?.wasEncrypted).toBe(true);
|
||||
});
|
||||
|
||||
it('rsa-modern.bin wird am Inhalt erkannt', () => {
|
||||
const r = detect('rsa-modern.bin', PASSWORD);
|
||||
expect(r.items.filter((i) => i.kind === 'certificate')).toHaveLength(3);
|
||||
expect(r.items.filter((i) => i.kind === 'privateKey')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('ohne Passwort: gesperrt mit Container pkcs12', () => {
|
||||
expect(detect('rsa-modern.pfx').locked).toEqual([
|
||||
{ file: 0, path: 'rsa-modern.pfx', container: 'pkcs12', reason: 'passwordNeeded' },
|
||||
]);
|
||||
expect(detect('rsa-modern.pfx', 'falsch').locked[0].reason).toBe('passwordWrong');
|
||||
});
|
||||
|
||||
it('das Passwort einer anderen Datei oeffnet auch diese Datei', () => {
|
||||
const r = detect('rsa-legacy.pfx', '', [PASSWORD]);
|
||||
expect(r.locked).toEqual([]);
|
||||
expect(r.items.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('die Antwort enthaelt das Passwort nicht', () => {
|
||||
expect(JSON.stringify(detect('rsa-modern.pfx', PASSWORD))).not.toContain(PASSWORD);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user