Files
tessera-ctl/apps/api/src/cert-manager/zip-expand.spec.ts
T
schalli 1554ae83c1 feat(cert-manager): Hersteller-ZIP, PKCS#7, eingefügter Text, Analysieren und Aufteilen
- ZIP wird an den Anfangsbytes erkannt und mit Grenzen geöffnet (eine Ebene, Verhältnis, Gesamtgröße, verschlüsselte Einträge)
- PKCS#7 als PEM und DER, auch für EC, über den ASN.1-Lauf
- Eingefügter PEM-Text als eigener Eintrag im Reiter Dateien
- Neue Reiter Analysieren und Aufteilen auf dem gemeinsamen Arbeitsbereich

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 15:15:59 +02:00

157 lines
6.2 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import AdmZip from 'adm-zip';
import { describe, expect, it } from 'vitest';
import { expandZip, isZip, ZIP_LIMITS } from './zip-expand';
const fx = (name: string) => readFileSync(join(__dirname, '__fixtures__', name));
function zipOf(entries: Record<string, Buffer | string>): Buffer {
const zip = new AdmZip();
for (const [name, content] of Object.entries(entries)) {
zip.addFile(name, Buffer.isBuffer(content) ? content : Buffer.from(content));
}
return zip.toBuffer();
}
function vendorZip(): Buffer {
const inner = zipOf({ 'x.txt': 'innen' });
const zip = new AdmZip();
zip.addFile('ServerCertificate.crt', fx('rsa-leaf.pem'));
zip.addFile('Intermediate/CA.crt', fx('rsa-inter.pem'));
zip.addFile('__MACOSX/._ServerCertificate.crt', Buffer.from('mac'));
zip.addFile('.DS_Store', Buffer.from('ds'));
zip.addFile('Thumbs.db', Buffer.from('thumbs'));
zip.addFile('Intermediate/', Buffer.alloc(0));
zip.addFile('readme.txt', Buffer.from('Bitte lesen'));
zip.addFile('inner.zip', inner);
return zip.toBuffer();
}
describe('isZip', () => {
it('erkennt ZIP an den Anfangsbytes, nicht am Namen', () => {
expect(isZip(vendorZip())).toBe(true);
expect(isZip(Buffer.from('PK\x05\x06rest', 'binary'))).toBe(true);
expect(isZip(fx('rsa-leaf.pem'))).toBe(false);
expect(isZip(Buffer.alloc(0))).toBe(false);
});
});
describe('expandZip: Hersteller-ZIP', () => {
const result = expandZip(vendorZip(), 'vendor.zip', 3);
it('liefert die Zertifikate und die Textdatei mit Pfad "zip/eintrag"', () => {
expect(result.blobs.map((b) => b.path).sort()).toEqual([
'vendor.zip/Intermediate/CA.crt',
'vendor.zip/ServerCertificate.crt',
'vendor.zip/readme.txt',
]);
const server = result.blobs.find((b) => b.path === 'vendor.zip/ServerCertificate.crt');
expect(server?.buffer.equals(fx('rsa-leaf.pem'))).toBe(true);
});
it('Muell (MACOSX, Punktdateien, Thumbs.db, Ordner) erzeugt nichts', () => {
const all = JSON.stringify(result);
expect(all).not.toContain('MACOSX');
expect(all).not.toContain('DS_Store');
expect(all).not.toContain('Thumbs');
});
it('ein ZIP im ZIP wird mit Grund gemeldet und nicht geoeffnet', () => {
expect(result.ignored).toEqual([
{ file: 3, path: 'vendor.zip/inner.zip', reason: 'nestedZip' },
]);
expect(result.blobs.some((b) => b.path.endsWith('inner.zip'))).toBe(false);
});
it('dasselbe ZIP wird auch unter anderem Namen geoeffnet (Anfangsbytes)', () => {
const renamed = expandZip(vendorZip(), 'bundle.dat', 0);
expect(renamed.blobs).toHaveLength(3);
expect(renamed.blobs[0].path.startsWith('bundle.dat/')).toBe(true);
});
});
describe('expandZip: Grenzen und Fehler', () => {
it('Zufallsbytes mit ZIP-Anfang ergeben brokenZip', () => {
const junk = Buffer.concat([Buffer.from('PK\x03\x04', 'binary'), Buffer.alloc(200, 7)]);
const r = expandZip(junk, 'kaputt.zip', 1);
expect(r.blobs).toEqual([]);
expect(r.ignored).toEqual([{ file: 1, path: 'kaputt.zip', reason: 'brokenZip' }]);
});
it('ein verschluesselter Eintrag wird mit Grund gemeldet', () => {
const r = expandZip(fx('encrypted-entry.zip'), 'enc.zip', 0);
expect(r.blobs).toEqual([]);
expect(r.ignored).toEqual([{ file: 0, path: 'enc.zip/rsa-leaf.pem', reason: 'encryptedZip' }]);
});
it('mehr Eintraege als erlaubt: ein tooManyEntries fuer das ganze ZIP, keine Teile', () => {
const zip = zipOf({ 'a.pem': 'a', 'b.pem': 'b', 'c.pem': 'c' });
const r = expandZip(zip, 'viele.zip', 2, { ...ZIP_LIMITS, maxEntries: 2 });
expect(r.blobs).toEqual([]);
expect(r.ignored).toEqual([{ file: 2, path: 'viele.zip', reason: 'tooManyEntries' }]);
});
it('Muell zaehlt nicht zu den erlaubten Eintraegen', () => {
const zip = zipOf({ 'a.pem': 'a', '.hidden': 'x', '__MACOSX/b': 'y', 'Thumbs.db': 'z' });
const r = expandZip(zip, 'z.zip', 0, { ...ZIP_LIMITS, maxEntries: 1 });
expect(r.blobs).toHaveLength(1);
expect(r.ignored).toEqual([]);
});
it('ein zu grosser Eintrag wird mit tooLarge uebersprungen, der Rest bleibt', () => {
const zip = zipOf({ 'gross.pem': Buffer.from('ab'.repeat(400)), 'klein.pem': 'k' });
const r = expandZip(zip, 'g.zip', 0, { ...ZIP_LIMITS, maxEntryBytes: 500 });
expect(r.ignored).toEqual([{ file: 0, path: 'g.zip/gross.pem', reason: 'tooLarge' }]);
expect(r.blobs.map((b) => b.path)).toEqual(['g.zip/klein.pem']);
});
it('600 kB Nullbytes (Verhaeltnis ueber 100) ergeben suspicious', () => {
const zip = zipOf({ 'null.bin': Buffer.alloc(600 * 1024) });
const r = expandZip(zip, 'bombe.zip', 0);
expect(r.blobs).toEqual([]);
expect(r.ignored).toEqual([{ file: 0, path: 'bombe.zip/null.bin', reason: 'suspicious' }]);
});
it('behaltene Eintraege ueber der Gesamtgrenze: ein zipTooLarge, keine Teile', () => {
const zip = zipOf({ 'a.pem': Buffer.from('1234567890'), 'b.pem': Buffer.from('1234567890') });
const r = expandZip(zip, 'summe.zip', 4, { ...ZIP_LIMITS, maxTotalBytes: 15 });
expect(r.blobs).toEqual([]);
expect(r.ignored).toEqual([{ file: 4, path: 'summe.zip', reason: 'zipTooLarge' }]);
});
it('prueft die Grenzen vor dem Entpacken (kein Entpacken bei zipTooLarge)', () => {
const zip = zipOf({ 'a.pem': Buffer.from('1234567890'), 'b.pem': Buffer.from('1234567890') });
const original = AdmZip.prototype.getEntries;
let inflated = 0;
AdmZip.prototype.getEntries = function patched(this: AdmZip) {
const entries = original.call(this);
for (const e of entries) {
const get = e.getData.bind(e);
e.getData = () => {
inflated++;
return get();
};
}
return entries;
};
try {
expandZip(zip, 's.zip', 0, { ...ZIP_LIMITS, maxTotalBytes: 15 });
} finally {
AdmZip.prototype.getEntries = original;
}
expect(inflated).toBe(0);
});
it('Anzeigepfad ohne Steuerzeichen', () => {
const zip = zipOf({ 'a\u0001b.pem': 'x' });
const r = expandZip(zip, 'c.zip', 0);
expect(r.blobs[0].path).toBe('c.zip/ab.pem');
});
it('leeres ZIP ergibt keine Teile und keinen Fehler', () => {
const r = expandZip(new AdmZip().toBuffer(), 'leer.zip', 0);
expect(r.blobs).toEqual([]);
});
});