feat(cert-manager): Reiter „Dateien“ mit mehreren Dateien und ein Parser für RSA und EC – Durchstich
- Ein gemeinsamer Arbeitsbereich im Browser: jede Auswahl hängt an, eine zweite Datei ersetzt nie die erste - Ein Parser (node:crypto) für RSA- und EC-Zertifikate als PEM, DER und TRUSTED CERTIFICATE, Rolle je Datei - Neue Route analyze (mehrere Dateien, 30 x 5 MiB, zusammen 20 MiB); alte Routen, Service und Web-Reiter entfernt - Test-PKI-Fixtures (RSA und EC, Ketten, Schlüssel, CSR, PFX) mit Erzeugungsskript Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { X509Certificate } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { certItemFromDer, detectBlob } from './cert-model';
|
||||
import type { CertItem } from './cert-types';
|
||||
|
||||
const fx = (name: string) => readFileSync(join(__dirname, '__fixtures__', name));
|
||||
const ctx = (path: string) => ({ file: 0, path, passwords: [] as string[] });
|
||||
|
||||
function certs(name: string): CertItem[] {
|
||||
const r = detectBlob(fx(name), ctx(name));
|
||||
return r.items.filter((i): i is CertItem => i.kind === 'certificate');
|
||||
}
|
||||
|
||||
describe('detectBlob: Zertifikate', () => {
|
||||
it('RSA-Serverzertifikat: alle Felder aus node:crypto', () => {
|
||||
const [c] = certs('rsa-leaf.pem');
|
||||
const x = new X509Certificate(fx('rsa-leaf.pem'));
|
||||
expect(c.cn).toBe('www.example.test');
|
||||
expect(c.san).toEqual(['www.example.test', 'example.test']);
|
||||
expect(c.issuerCn).toBe('Tessera Test Inter RSA');
|
||||
expect(c.role).toBe('end-entity');
|
||||
expect(c.keyType).toBe('RSA');
|
||||
expect(c.keyBits).toBe(2048);
|
||||
expect(c.curve).toBeNull();
|
||||
expect(c.isCa).toBe(false);
|
||||
expect(c.selfSigned).toBe(false);
|
||||
expect(c.aiaIssuerUrls).toEqual(['http://pki.example.test/rsa-inter.cer']);
|
||||
expect(c.sha256).toBe(x.fingerprint256);
|
||||
expect(c.sha1).toBe(x.fingerprint);
|
||||
expect(c.id).toBe(`c-${x.fingerprint256.replace(/:/g, '').slice(0, 16).toLowerCase()}`);
|
||||
expect(c.sources).toEqual([{ file: 0, path: 'rsa-leaf.pem' }]);
|
||||
expect(c.pem.startsWith('-----BEGIN CERTIFICATE-----')).toBe(true);
|
||||
expect(c.baseName).toBe('www.example.test');
|
||||
expect(c.keyId).toMatch(/^k-[0-9a-f]{16}$/);
|
||||
expect(c.isExpired).toBe(false);
|
||||
expect(c.daysLeft).toBeGreaterThan(30000);
|
||||
});
|
||||
|
||||
it('Zwischenzertifikat und Stammzertifikat bekommen ihre Rolle', () => {
|
||||
expect(certs('rsa-inter.pem')[0].role).toBe('intermediate');
|
||||
expect(certs('rsa-inter.pem')[0].baseName).toBe('Tessera_Test_Inter_RSA');
|
||||
const root = certs('rsa-root.pem')[0];
|
||||
expect(root.role).toBe('root');
|
||||
expect(root.selfSigned).toBe(true);
|
||||
expect(root.isCa).toBe(true);
|
||||
});
|
||||
|
||||
it('EC-Zertifikate: Schluesseltyp und Kurve', () => {
|
||||
const leaf = certs('ec-leaf.pem')[0];
|
||||
expect(leaf.keyType).toBe('EC');
|
||||
expect(leaf.curve).toBe('P-256');
|
||||
expect(leaf.keyBits).toBe(256);
|
||||
const root = certs('ec-root.pem')[0];
|
||||
expect(root.curve).toBe('P-384');
|
||||
expect(root.keyBits).toBe(384);
|
||||
expect(root.isCa).toBe(true);
|
||||
expect(root.selfSigned).toBe(true);
|
||||
expect(root.role).toBe('root');
|
||||
});
|
||||
|
||||
it('selbstsigniert, aber keine CA: bleibt Serverzertifikat', () => {
|
||||
const c = certs('selfsigned-leaf.pem')[0];
|
||||
expect(c.selfSigned).toBe(true);
|
||||
expect(c.isCa).toBe(false);
|
||||
expect(c.role).toBe('end-entity');
|
||||
});
|
||||
|
||||
it('abgelaufenes Zwischenzertifikat wird als abgelaufen gemeldet', () => {
|
||||
const c = certs('rsa-inter-expired.pem')[0];
|
||||
expect(c.isExpired).toBe(true);
|
||||
expect(c.daysLeft).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('DER ergibt dieselbe Kennung wie PEM', () => {
|
||||
expect(certs('ec-leaf.cer')[0].id).toBe(certs('ec-leaf.pem')[0].id);
|
||||
expect(certs('rsa-leaf.cer')[0].id).toBe(certs('rsa-leaf.pem')[0].id);
|
||||
});
|
||||
|
||||
it('Fullchain mit BOM, CRLF und Text drumherum: drei Zertifikate', () => {
|
||||
const body = fx('ec-fullchain.pem').toString('utf8').replace(/\n/g, '\r\n');
|
||||
const messy = Buffer.concat([
|
||||
Buffer.from([0xef, 0xbb, 0xbf]),
|
||||
Buffer.from(`Bag Attributes\r\n friendlyName: x\r\n${body}\r\nEnde der Datei\r\n`, 'utf8'),
|
||||
]);
|
||||
const r = detectBlob(messy, ctx('messy.pem'));
|
||||
expect(r.items).toHaveLength(3);
|
||||
expect(r.ignored).toEqual([]);
|
||||
expect(r.items.map((i) => (i as CertItem).role).sort()).toEqual([
|
||||
'end-entity',
|
||||
'intermediate',
|
||||
'root',
|
||||
]);
|
||||
});
|
||||
|
||||
it('TRUSTED CERTIFICATE liefert das Zertifikat', () => {
|
||||
const [c] = certs('rsa-trusted.pem');
|
||||
expect(c.id).toBe(certs('rsa-leaf.pem')[0].id);
|
||||
});
|
||||
|
||||
it('kaputte Eingaben werden gemeldet, ohne zu werfen', () => {
|
||||
const half = fx('rsa-leaf.cer').subarray(0, 300);
|
||||
const brokenBase64 = Buffer.from(
|
||||
'-----BEGIN CERTIFICATE-----\nMIIDzTCC@@@@@@@@@!!!\n-----END CERTIFICATE-----\n',
|
||||
);
|
||||
const random = Buffer.from(Array.from({ length: 512 }, (_, i) => (i * 37 + 11) % 256));
|
||||
for (const blob of [random, Buffer.alloc(0), half, brokenBase64, Buffer.from('hallo welt')]) {
|
||||
const r = detectBlob(blob, ctx('x.bin'));
|
||||
expect(r.items).toEqual([]);
|
||||
expect(r.ignored).toEqual([{ file: 0, path: 'x.bin', reason: 'unknown' }]);
|
||||
}
|
||||
});
|
||||
|
||||
it('ein kaputter Block neben einem guten verhindert das Zertifikat nicht', () => {
|
||||
const mixed = Buffer.concat([
|
||||
Buffer.from('-----BEGIN CERTIFICATE-----\n@@@@\n-----END CERTIFICATE-----\n'),
|
||||
fx('rsa-leaf.pem'),
|
||||
]);
|
||||
const r = detectBlob(mixed, ctx('mixed.pem'));
|
||||
expect(r.items).toHaveLength(1);
|
||||
expect(r.ignored).toEqual([]);
|
||||
});
|
||||
|
||||
it('certItemFromDer erzeugt Kennung und Quelle', () => {
|
||||
const der = fx('rsa-leaf.cer');
|
||||
const item = certItemFromDer(der, { file: 3, path: 'a/b.cer' });
|
||||
expect(item.sources).toEqual([{ file: 3, path: 'a/b.cer' }]);
|
||||
expect(item.id).toMatch(/^c-[0-9a-f]{16}$/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user