feat(cert-manager): Zusammenführen mit Fullchain und Nur Kette
- Kettenbau mit Aussteller- und Signaturprüfung (checkIssued plus verify), Wurzel nur auf Wunsch - build-Route für Fullchain und Nur Kette (PEM), eigene JSON-Grenze 512 KiB mit 413 und Code tooLarge - Reiter Zusammenführen mit Kettenansicht, Hinweis bei fehlendem Zwischenzertifikat Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildChains } from './cert-chain';
|
||||
import { detectBlob } from './cert-model';
|
||||
import type { CertItem } from './cert-types';
|
||||
|
||||
const fx = (name: string) => readFileSync(join(__dirname, '__fixtures__', name));
|
||||
|
||||
function load(...names: string[]): CertItem[] {
|
||||
const items: CertItem[] = [];
|
||||
names.forEach((name, file) => {
|
||||
const result = detectBlob(fx(name), { file, path: name, passwords: [] });
|
||||
for (const item of result.items) if (item.kind === 'certificate') items.push(item);
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
const cnOf = (certs: CertItem[], id: string) => certs.find((c) => c.id === id)?.cn;
|
||||
const pathCns = (certs: CertItem[], path: string[]) => path.map((id) => cnOf(certs, id));
|
||||
|
||||
describe('buildChains', () => {
|
||||
it('Server + Zwischenzertifikat ohne Wurzel: unvollstaendig, Luecke nach der CA', () => {
|
||||
const certs = load('rsa-leaf.pem', 'rsa-inter.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
expect(chains).toHaveLength(1);
|
||||
expect(pathCns(certs, chains[0].path)).toEqual(['www.example.test', 'Tessera Test Inter RSA']);
|
||||
expect(chains[0].complete).toBe(false);
|
||||
expect(chains[0].rootId).toBeNull();
|
||||
expect(chains[0].gap).toMatchObject({
|
||||
kind: 'afterCa',
|
||||
missingIssuerCn: 'Tessera Test Root RSA',
|
||||
});
|
||||
});
|
||||
|
||||
it('mit Wurzel: vollstaendig, rootId gesetzt, keine Luecke', () => {
|
||||
const certs = load('rsa-leaf.pem', 'rsa-inter.pem', 'rsa-root.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
expect(chains).toHaveLength(1);
|
||||
expect(chains[0].complete).toBe(true);
|
||||
expect(cnOf(certs, chains[0].rootId as string)).toBe('Tessera Test Root RSA');
|
||||
expect(chains[0].gap).toBeNull();
|
||||
expect(chains[0].path).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('nur das Serverzertifikat: Luecke nach dem Server mit Adresse des Ausstellers', () => {
|
||||
const certs = load('rsa-leaf.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
expect(chains[0].gap).toMatchObject({
|
||||
kind: 'afterLeaf',
|
||||
missingIssuerCn: 'Tessera Test Inter RSA',
|
||||
aiaUrls: ['http://pki.example.test/rsa-inter.cer'],
|
||||
});
|
||||
});
|
||||
|
||||
it('gleichnamige CA mit anderem Schluessel (Attrappe) wird nie genommen', () => {
|
||||
const certs = load(
|
||||
'rsa-leaf-noaki.pem',
|
||||
'rsa-inter-decoy.pem',
|
||||
'rsa-inter.pem',
|
||||
'rsa-root.pem',
|
||||
);
|
||||
const { chains } = buildChains(certs);
|
||||
expect(chains).toHaveLength(1);
|
||||
const names = chains[0].path.map((id) => certs.find((c) => c.id === id));
|
||||
expect(names[1]?.sources[0].path).toBe('rsa-inter.pem');
|
||||
expect(chains[0].complete).toBe(true);
|
||||
});
|
||||
|
||||
it('nur die Attrappe vorhanden: die Signaturpruefung lehnt sie ab, Luecke nach dem Server', () => {
|
||||
const certs = load('rsa-leaf-noaki.pem', 'rsa-inter-decoy.pem', 'rsa-root.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
expect(chains).toHaveLength(1);
|
||||
expect(chains[0].path).toHaveLength(1);
|
||||
expect(chains[0].gap?.kind).toBe('afterLeaf');
|
||||
});
|
||||
|
||||
it('kreuzsigniertes Zwischenzertifikat fuehrt zur zweiten Wurzel', () => {
|
||||
const certs = load('rsa-leaf.pem', 'rsa-inter-cross.pem', 'rsa-root2.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
expect(chains[0].complete).toBe(true);
|
||||
expect(cnOf(certs, chains[0].rootId as string)).toBe('Tessera Test Root RSA 2');
|
||||
});
|
||||
|
||||
it('beide Varianten vorhanden: Hauptkette ueber rsa-inter und rsa-root, mindestens eine Alternative', () => {
|
||||
const certs = load('rsa-leaf.pem', 'rsa-inter.pem', 'rsa-inter-cross.pem', 'rsa-root.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
expect(chains).toHaveLength(1);
|
||||
expect(chains[0].complete).toBe(true);
|
||||
expect(cnOf(certs, chains[0].rootId as string)).toBe('Tessera Test Root RSA');
|
||||
expect(chains[0].alternatives).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('abgelaufenes Zwischenzertifikat wird nicht als Hauptkette genommen', () => {
|
||||
const certs = load('rsa-leaf.pem', 'rsa-inter-expired.pem', 'rsa-inter.pem', 'rsa-root.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
const middle = certs.find((c) => c.id === chains[0].path[1]);
|
||||
expect(middle?.isExpired).toBe(false);
|
||||
expect(middle?.sources[0].path).toBe('rsa-inter.pem');
|
||||
expect(chains[0].alternatives).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('die Reihenfolge der Eingabe aendert die Hauptkette nicht', () => {
|
||||
const names = ['rsa-leaf.pem', 'rsa-inter-expired.pem', 'rsa-inter.pem', 'rsa-root.pem'];
|
||||
const forward = load(...names);
|
||||
const reversed = load(...[...names].reverse());
|
||||
const a = buildChains(forward).chains[0];
|
||||
const b = buildChains(reversed).chains[0];
|
||||
expect(a.path).toEqual(b.path);
|
||||
expect(a.alternatives).toBe(b.alternatives);
|
||||
});
|
||||
|
||||
it('Zwischenzertifikat + Wurzel ohne Serverzertifikat: eine Kette mit dem Zwischenzertifikat als Kopf', () => {
|
||||
const certs = load('rsa-inter.pem', 'rsa-root.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
expect(chains).toHaveLength(1);
|
||||
expect(cnOf(certs, chains[0].headId)).toBe('Tessera Test Inter RSA');
|
||||
expect(chains[0].complete).toBe(true);
|
||||
});
|
||||
|
||||
it('selbstsigniertes Serverzertifikat: Kette aus ihm selbst, vollstaendig, ohne Wurzel', () => {
|
||||
const certs = load('selfsigned-leaf.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
expect(chains[0].path).toEqual([certs[0].id]);
|
||||
expect(chains[0].complete).toBe(true);
|
||||
expect(chains[0].rootId).toBeNull();
|
||||
expect(chains[0].gap).toBeNull();
|
||||
});
|
||||
|
||||
it('EC-Kette wird genauso gebaut', () => {
|
||||
const certs = load('ec-leaf.pem', 'ec-inter.pem', 'ec-root.pem');
|
||||
const { chains } = buildChains(certs);
|
||||
expect(pathCns(certs, chains[0].path)).toEqual([
|
||||
'ec.example.test',
|
||||
'Tessera Test Inter EC',
|
||||
'Tessera Test Root EC',
|
||||
]);
|
||||
expect(chains[0].complete).toBe(true);
|
||||
});
|
||||
|
||||
it('mit vorgegebenem Kopf wird genau diese Kette gebaut', () => {
|
||||
const certs = load('rsa-leaf.pem', 'rsa-inter.pem', 'rsa-root.pem');
|
||||
const inter = certs.find((c) => c.role === 'intermediate') as CertItem;
|
||||
const { chains } = buildChains(certs, [inter.id]);
|
||||
expect(chains).toHaveLength(1);
|
||||
expect(chains[0].headId).toBe(inter.id);
|
||||
});
|
||||
|
||||
it('leere Menge ergibt keine Ketten', () => {
|
||||
expect(buildChains([])).toEqual({ chains: [] });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user