Files
tessera-ctl/apps/api/src/cert-manager/cert-manager.service.spec.ts
T
schalli b4aaed4b7c fix(quick-260921-iwr): drei bag.cert-Zusicherungen in cert-manager.service.ts zu echten Waechtern gemacht
node-forge setzt bag.cert bei einem wohlgeformten, aber nicht als X.509
lesbaren Zertifikats-Bag auf null (lib/pkcs12.js Zeile 703-709). Die drei
Zusicherungen in parseCert, mergeCerts und convertCert behaupteten also
etwas Falsches, obwohl die Folge bereits behandelt war: alle vier Pfade
antworteten schon vorher mit 400, nie mit 500 (mit einer selbst gebauten
83-Byte-PFX nachgemessen).

Ersetzt die drei Zusicherungen durch ausdrueckliche Pruefungen mit
praeziser BadRequestException. In mergeCerts wird kein Zertifikat mehr
verschluckt: alle Bags werden auf Vollstaendigkeit geprueft, bevor die
Liste ueber ein Typpraedikat zurueckgegeben wird.

RED-Tests zuerst geschrieben und mit den heutigen Meldungen ("Failed to
extract certificate details" / "Failed to create merged certificate
output" / "Failed to convert certificate to pem: serialization error")
rot bestaetigt, dann die Waechter ergaenzt: GREEN.

Verhaltensaenderung ausdruecklich beabsichtigt (D-02): nur der Text der
Fehlermeldung fuer diese eine Eingabeklasse aendert sich, der Statuscode
bleibt in allen vier Pfaden 400.

NONNULL 11 -> 8, davon 3 in cert-manager.service.ts (Zeilen 133/516/661
bleiben unveraendert — durch Hash-Laenge bzw. vorgelagerte Passwort-
Pruefung garantiert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:01:36 +02:00

779 lines
30 KiB
TypeScript

import { BadRequestException } from '@nestjs/common';
import * as forge from 'node-forge';
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { seedCertManagerModule } from './cert-manager.seed';
import { CertManagerService } from './cert-manager.service';
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
/** Generate a self-signed X.509 cert via node-forge (RSA 1024 — fast for tests) */
function generateSelfSignedCert(): forge.pki.Certificate {
const keys = forge.pki.rsa.generateKeyPair(1024);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [{ name: 'commonName', value: 'test.example.com' }];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(keys.privateKey, forge.md.sha256.create());
return cert;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('seedCertManagerModule', () => {
it('calls moduleRegistryService.seedModule once with cert-manager slug', async () => {
const mockSeedModule = vi.fn().mockResolvedValue(undefined);
const mockModuleRegistryService = { seedModule: mockSeedModule } as any;
await seedCertManagerModule(mockModuleRegistryService);
expect(mockSeedModule).toHaveBeenCalledTimes(1);
const arg = mockSeedModule.mock.calls[0][0];
expect(arg.slug).toBe('cert-manager');
expect(arg.category).toBe('security-tools');
expect(arg.isSystem).toBe(true);
});
});
describe('CertManagerService helpers', () => {
let service: CertManagerService;
let testCert: forge.pki.Certificate;
beforeAll(() => {
service = new CertManagerService();
testCert = generateSelfSignedCert();
});
// -------------------------------------------------------------------------
// detectFormat
// -------------------------------------------------------------------------
describe('detectFormat', () => {
it('returns "pfx" for .pfx extension', () => {
const buf = Buffer.from([0x30, 0x82]); // arbitrary binary
expect(service.detectFormat('cert.pfx', buf)).toBe('pfx');
});
it('returns "p7b" for .p7b extension', () => {
const buf = Buffer.from([0x30, 0x82]);
expect(service.detectFormat('cert.p7b', buf)).toBe('p7b');
});
it('returns "der" for .der extension', () => {
const buf = Buffer.from([0x30, 0x82]);
expect(service.detectFormat('cert.der', buf)).toBe('der');
});
it('returns "pem" for .pem extension', () => {
const buf = Buffer.from('-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----');
expect(service.detectFormat('cert.pem', buf)).toBe('pem');
});
it('returns "pem" for .cer extension with PEM content', () => {
const buf = Buffer.from('-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----');
expect(service.detectFormat('cert.cer', buf)).toBe('pem');
});
it('returns "der" for .cer extension with binary (non-PEM) content', () => {
const buf = Buffer.from([0x30, 0x82, 0x01, 0x00]);
expect(service.detectFormat('cert.cer', buf)).toBe('der');
});
});
// -------------------------------------------------------------------------
// getFingerprint
// -------------------------------------------------------------------------
describe('getFingerprint', () => {
it('returns uppercase colon-separated hex for sha256', () => {
const fp = service.getFingerprint(testCert, 'sha256');
// e.g. "AA:BB:CC:..."
expect(fp).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/);
});
it('returns uppercase colon-separated hex for sha1', () => {
const fp = service.getFingerprint(testCert, 'sha1');
expect(fp).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/);
});
});
// -------------------------------------------------------------------------
// parsePemChain
// -------------------------------------------------------------------------
describe('parsePemChain', () => {
it('returns array of length 2 for two concatenated cert PEMs', () => {
const cert1 = generateSelfSignedCert();
const cert2 = generateSelfSignedCert();
const pem1 = forge.pki.certificateToPem(cert1);
const pem2 = forge.pki.certificateToPem(cert2);
const chain = `${pem1}\n${pem2}`;
const parsed = service.parsePemChain(chain);
expect(parsed).toHaveLength(2);
});
it('returns array of length 1 for a single PEM', () => {
const pem = forge.pki.certificateToPem(testCert);
const parsed = service.parsePemChain(pem);
expect(parsed).toHaveLength(1);
});
});
});
// ---------------------------------------------------------------------------
// parseCert — RED tests (CERT-01, CERT-05 read half)
// ---------------------------------------------------------------------------
describe('parseCert', () => {
let service: CertManagerService;
let certPem: string;
let certDerBuffer: Buffer;
let pfxBuffer: Buffer;
const CN = 'parsecert.example.com';
beforeAll(() => {
service = new CertManagerService();
// Create a cert+key pair for all fixtures (RSA-1024 for speed)
const keys = forge.pki.rsa.generateKeyPair(1024);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [{ name: 'commonName', value: CN }];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(keys.privateKey, forge.md.sha256.create());
// PEM fixture
certPem = forge.pki.certificateToPem(cert);
// DER fixture — binary encoding required (Pitfall 1)
const derBytes = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
certDerBuffer = Buffer.from(derBytes, 'binary');
// PFX fixture with password 'secret'
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
keys.privateKey,
[cert],
'secret',
{ algorithm: '3des' },
);
const p12DerBytes = forge.asn1.toDer(p12Asn1).getBytes();
pfxBuffer = Buffer.from(p12DerBytes, 'binary');
}, 15000); // 15s timeout — RSA keygen can be slow in pure JS
it('returns CertDetails with subject.cn, fingerprint.sha256, keyType RSA, keyBits, isExpired false for PEM input', async () => {
// RED: parseCert currently throws NotImplementedException — this test will FAIL
const result = await (service.parseCert({ pemText: certPem }) as Promise<any>);
expect(result.subject.cn).toBe(CN);
expect(result.fingerprint.sha256).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/);
expect(result.keyType).toBe('RSA');
expect(result.keyBits).toBe(1024);
expect(result.validity.isExpired).toBe(false);
});
it('returns CertDetails with matching subject.cn for DER input', async () => {
// RED: parseCert throws NotImplementedException — this test will FAIL
const result = await (service.parseCert({
file: { originalname: 'c.der', buffer: certDerBuffer },
}) as Promise<any>);
expect(result.subject.cn).toBe(CN);
});
it('returns CertDetails for PFX with correct password', async () => {
// RED: parseCert throws NotImplementedException — this test will FAIL
const result = await (service.parseCert({
file: { originalname: 'c.pfx', buffer: pfxBuffer },
password: 'secret',
}) as Promise<any>);
expect(result.subject.cn).toBe(CN);
});
it('throws BadRequestException for PFX with wrong password', async () => {
// RED: currently throws NotImplementedException (not BadRequestException) — FAIL
await expect(
service.parseCert({
file: { originalname: 'c.pfx', buffer: pfxBuffer },
password: 'wrong',
}),
).rejects.toThrow(BadRequestException);
});
it('throws BadRequestException for malformed PEM input', async () => {
// RED: currently throws NotImplementedException (not BadRequestException) — FAIL
await expect(
service.parseCert({ pemText: 'not a cert' }),
).rejects.toThrow(BadRequestException);
});
});
// ---------------------------------------------------------------------------
// splitCerts — RED tests (CERT-02)
// ---------------------------------------------------------------------------
describe('splitCerts', () => {
let service: CertManagerService;
let cert1Pem: string;
let cert2Pem: string;
let fullchainBuffer: Buffer;
let p7bBuffer: Buffer;
const CN1 = 'split1.example.com';
const CN2 = 'split2.example.com';
beforeAll(() => {
service = new CertManagerService();
// Build cert 1 (RSA-1024 for speed)
const keys1 = forge.pki.rsa.generateKeyPair(1024);
const c1 = forge.pki.createCertificate();
c1.publicKey = keys1.publicKey;
c1.serialNumber = '01';
c1.validity.notBefore = new Date();
c1.validity.notAfter = new Date();
c1.validity.notAfter.setFullYear(c1.validity.notBefore.getFullYear() + 1);
const attrs1 = [{ name: 'commonName', value: CN1 }];
c1.setSubject(attrs1);
c1.setIssuer(attrs1);
c1.sign(keys1.privateKey, forge.md.sha256.create());
cert1Pem = forge.pki.certificateToPem(c1);
// Build cert 2
const keys2 = forge.pki.rsa.generateKeyPair(1024);
const c2 = forge.pki.createCertificate();
c2.publicKey = keys2.publicKey;
c2.serialNumber = '02';
c2.validity.notBefore = new Date();
c2.validity.notAfter = new Date();
c2.validity.notAfter.setFullYear(c2.validity.notBefore.getFullYear() + 1);
const attrs2 = [{ name: 'commonName', value: CN2 }];
c2.setSubject(attrs2);
c2.setIssuer(attrs2);
c2.sign(keys2.privateKey, forge.md.sha256.create());
cert2Pem = forge.pki.certificateToPem(c2);
// Fullchain fixture: two PEMs concatenated
const fullchainPem = `${cert1Pem}\n${cert2Pem}`;
fullchainBuffer = Buffer.from(fullchainPem, 'utf-8');
// P7B fixture: PEM-wrapped PKCS7 SignedData bundle with both certs
const p7 = forge.pkcs7.createSignedData();
p7.addCertificate(c1);
p7.addCertificate(c2);
const p7DerBytes = forge.asn1.toDer(p7.toAsn1()).getBytes();
const p7PemStr = forge.pem.encode({ type: 'PKCS7', body: p7DerBytes });
p7bBuffer = Buffer.from(p7PemStr, 'utf-8');
}, 30000); // 30s — two RSA-1024 keygens
it('returns count 2 and two certs each with a single PEM block and correct CN for fullchain PEM', async () => {
// RED: splitCerts throws NotImplementedException — FAIL
const result = await (service.splitCerts({
file: { originalname: 'fullchain.pem', buffer: fullchainBuffer },
}) as Promise<any>);
expect(result.count).toBe(2);
expect(result.certs).toHaveLength(2);
// Each cert content decodes to exactly one BEGIN CERTIFICATE block
for (const entry of result.certs) {
const decoded = Buffer.from(entry.content, 'base64').toString('utf-8');
const matches = decoded.match(/-----BEGIN CERTIFICATE-----/g);
expect(matches).toHaveLength(1);
}
// CN values are present
const cns = result.certs.map((c: any) => c.subject.cn);
expect(cns).toContain(CN1);
expect(cns).toContain(CN2);
// validity.notAfter is present and looks like ISO 8601
for (const entry of result.certs) {
expect(entry.validity.notAfter).toMatch(/^\d{4}-\d{2}-\d{2}T/);
}
});
it('returns enclosed certs from a P7B PEM bundle', async () => {
// RED: splitCerts throws NotImplementedException — FAIL
const result = await (service.splitCerts({
file: { originalname: 'bundle.p7b', buffer: p7bBuffer },
}) as Promise<any>);
expect(result.count).toBeGreaterThanOrEqual(1);
expect(result.certs.length).toBeGreaterThanOrEqual(1);
});
it('throws BadRequestException for malformed input', async () => {
// RED: splitCerts throws NotImplementedException (not BadRequestException) — FAIL
await expect(
service.splitCerts({
file: { originalname: 'bad.pem', buffer: Buffer.from('this is garbage') },
}),
).rejects.toThrow(BadRequestException);
});
});
// ---------------------------------------------------------------------------
// mergeCerts — RED tests (CERT-03, CERT-05 write half)
// ---------------------------------------------------------------------------
describe('mergeCerts', () => {
let service: CertManagerService;
let certAPem: string;
let certBPem: string;
const CNA = 'merge-a.example.com';
const CNB = 'merge-b.example.com';
beforeAll(() => {
service = new CertManagerService();
// Cert A (RSA-1024 for speed)
const keysA = forge.pki.rsa.generateKeyPair(1024);
const certA = forge.pki.createCertificate();
certA.publicKey = keysA.publicKey;
certA.serialNumber = '01';
certA.validity.notBefore = new Date();
certA.validity.notAfter = new Date();
certA.validity.notAfter.setFullYear(certA.validity.notBefore.getFullYear() + 1);
const attrsA = [{ name: 'commonName', value: CNA }];
certA.setSubject(attrsA);
certA.setIssuer(attrsA);
certA.sign(keysA.privateKey, forge.md.sha256.create());
certAPem = forge.pki.certificateToPem(certA);
// Cert B
const keysB = forge.pki.rsa.generateKeyPair(1024);
const certB = forge.pki.createCertificate();
certB.publicKey = keysB.publicKey;
certB.serialNumber = '02';
certB.validity.notBefore = new Date();
certB.validity.notAfter = new Date();
certB.validity.notAfter.setFullYear(certB.validity.notBefore.getFullYear() + 1);
const attrsB = [{ name: 'commonName', value: CNB }];
certB.setSubject(attrsB);
certB.setIssuer(attrsB);
certB.sign(keysB.privateKey, forge.md.sha256.create());
certBPem = forge.pki.certificateToPem(certB);
}, 30000); // 30s — two RSA-1024 keygens
it('returns PEM chain with 2 BEGIN CERTIFICATE blocks when merging 2 PEM files', async () => {
// RED: mergeCerts throws NotImplementedException — FAIL
const result = await (service.mergeCerts({
files: [
{ originalname: 'certA.pem', buffer: Buffer.from(certAPem, 'utf-8') },
{ originalname: 'certB.pem', buffer: Buffer.from(certBPem, 'utf-8') },
],
outputFormat: 'pem',
}) as Promise<any>);
expect(result.mimeType).toBe('application/x-pem-file');
expect(result.filename).toContain('chain');
// Decoded content must have exactly 2 cert blocks
const decoded = Buffer.from(result.content as string, 'base64').toString('utf-8');
const blocks = decoded.match(/-----BEGIN CERTIFICATE-----/g);
expect(blocks).toHaveLength(2);
});
it('returns password-protected PFX that round-trips with the correct password', async () => {
// RED: mergeCerts throws NotImplementedException — FAIL
// Note: 2-file minimum is enforced at the controller level; service accepts 1 file for PFX
const result = await (service.mergeCerts({
files: [
{ originalname: 'certA.pem', buffer: Buffer.from(certAPem, 'utf-8') },
],
outputFormat: 'pfx',
password: 'secret',
}) as Promise<any>);
expect(result.mimeType).toBe('application/x-pkcs12');
expect(result.filename).toBe('bundle.pfx');
// Round-trip: decode base64 PFX → re-parse with password 'secret' → verify cert bag present
const pfxBuf = Buffer.from(result.content as string, 'base64');
const p12Asn1 = forge.asn1.fromDer(
forge.util.createBuffer(pfxBuf.toString('binary')),
);
// Should NOT throw with the correct password (Open Question 1 resolution)
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, 'secret');
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
const bags = certBags[forge.pki.oids.certBag] ?? [];
expect(bags.length).toBeGreaterThanOrEqual(1);
});
it('throws BadRequestException when PFX output is requested without a password', async () => {
// RED: mergeCerts throws NotImplementedException (not BadRequestException) — FAIL
await expect(
service.mergeCerts({
files: [
{ originalname: 'certA.pem', buffer: Buffer.from(certAPem, 'utf-8') },
{ originalname: 'certB.pem', buffer: Buffer.from(certBPem, 'utf-8') },
],
outputFormat: 'pfx',
// no password — should be rejected
}),
).rejects.toThrow(BadRequestException);
});
it('throws BadRequestException for malformed (garbage) input files', async () => {
// RED: mergeCerts throws NotImplementedException (not BadRequestException) — FAIL
await expect(
service.mergeCerts({
files: [
{ originalname: 'bad.pem', buffer: Buffer.from('this is garbage') },
{ originalname: 'bad2.pem', buffer: Buffer.from('also garbage') },
],
outputFormat: 'pem',
}),
).rejects.toThrow(BadRequestException);
});
});
// ---------------------------------------------------------------------------
// convertCert — RED tests (CERT-04)
// ---------------------------------------------------------------------------
describe('convertCert', () => {
let service: CertManagerService;
let certPem: string;
let certDerBuffer: Buffer;
const CN = 'convert.example.com';
beforeAll(() => {
service = new CertManagerService();
// RSA-1024 self-signed cert (fast for tests)
const keys = forge.pki.rsa.generateKeyPair(1024);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [{ name: 'commonName', value: CN }];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(keys.privateKey, forge.md.sha256.create());
// PEM fixture
certPem = forge.pki.certificateToPem(cert);
// DER fixture — binary encoding required (Pitfall 1)
const derBytes = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
certDerBuffer = Buffer.from(derBytes, 'binary');
}, 15000);
it('PEM→DER: returns FileResponse with correct mimeType and round-trip identity', async () => {
// RED: convertCert throws NotImplementedException — this test will FAIL
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await (service.convertCert({ pemText: certPem, targetFormat: 'der' } as any) as Promise<any>);
expect(result.mimeType).toBe('application/x-x509-ca-cert');
expect(result.filename).toContain('converted');
expect(result.content).toBeTruthy();
// Round-trip identity: decode base64 DER → re-parse → verify CN matches original
const derBuf = Buffer.from(result.content as string, 'base64');
const asn1 = forge.asn1.fromDer(forge.util.createBuffer(derBuf.toString('binary')));
const reparsed = forge.pki.certificateFromAsn1(asn1);
expect(reparsed.subject.getField('CN')?.value).toBe(CN);
});
it('DER→PEM: returned PEM parses to cert with same CN (round-trip identity)', async () => {
// RED: convertCert throws NotImplementedException — this test will FAIL
const result = await (service.convertCert({
file: { originalname: 'c.der', buffer: certDerBuffer },
targetFormat: 'pem',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any) as Promise<any>);
expect(result.mimeType).toBe('application/x-pem-file');
// Decode base64 PEM content → parse → verify CN matches original
const pemStr = Buffer.from(result.content as string, 'base64').toString('utf-8');
const reparsed = forge.pki.certificateFromPem(pemStr);
expect(reparsed.subject.getField('CN')?.value).toBe(CN);
});
it('PEM→P7B: returned P7B contains the original certificate', async () => {
// RED: convertCert throws NotImplementedException — this test will FAIL
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await (service.convertCert({ pemText: certPem, targetFormat: 'p7b' } as any) as Promise<any>);
expect(result.mimeType).toBe('application/x-pkcs7-certificates');
expect(result.filename).toContain('converted');
// Decode base64 P7B (PEM-wrapped PKCS7) and verify at least 1 cert enclosed
const p7bContent = Buffer.from(result.content as string, 'base64').toString('utf-8');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const p7 = forge.pkcs7.messageFromPem(p7bContent) as any;
expect((p7.certificates as forge.pki.Certificate[]).length).toBeGreaterThanOrEqual(1);
});
it('throws BadRequestException for malformed PEM input', async () => {
// RED: convertCert throws NotImplementedException (not BadRequestException) — FAIL
await expect(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
service.convertCert({ pemText: 'garbage input not a cert', targetFormat: 'der' } as any),
).rejects.toThrow(BadRequestException);
});
});
// ---------------------------------------------------------------------------
// PFX mit unlesbarem Zertifikats-Bag — bag.cert = null (quick-260921-iwr, D-01/D-04)
//
// node-forge setzt bag.cert bei einem wohlgeformten, aber nicht als X.509
// lesbaren Zertifikats-Bag auf null (lib/pkcs12.js Zeile 703-709: der Fehler
// aus certificateFromAsn1 wird abgefangen und durch bag.cert = null ersetzt).
// Die drei betroffenen Zusicherungen (parseCert Zeile 207, mergeCerts Zeile
// 469, convertCert Zeile 602) behaupten also etwas, das node-forge selbst
// widerlegt. Alle vier Pfade antworteten schon vorher mit 400 statt 500 — die
// hier gepruefte Aenderung ist die Meldung, nicht der Statuscode (D-02).
// ---------------------------------------------------------------------------
describe('PFX mit unlesbarem Zertifikats-Bag (bag.cert = null)', () => {
let malformedPfxBuffer: Buffer;
beforeAll(() => {
// Handgebaute ~83-Byte-PFX-Datei mit forge.asn1: wohlgeformte DER-Struktur
// von aussen nach innen (PFX -> ContentInfo -> AuthenticatedSafe ->
// ContentInfo -> SafeContents -> SafeBag -> CertBag), deren
// Zertifikats-Bag-Inhalt aber nur "SEQUENCE { INTEGER 1 }" ist — kein
// X.509-Zertifikat. Passwort ist die leere Zeichenkette.
const fakeCertAsn1 = forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.SEQUENCE,
true,
[forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, String.fromCharCode(1))],
);
const fakeCertDer = forge.asn1.toDer(fakeCertAsn1).getBytes();
const certBagAsn1 = forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.SEQUENCE,
true,
[
forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.OID,
false,
forge.asn1.oidToDer(forge.pki.oids.x509Certificate).getBytes(),
),
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING, false, fakeCertDer),
]),
],
);
const safeBagAsn1 = forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.SEQUENCE,
true,
[
forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.OID,
false,
forge.asn1.oidToDer(forge.pki.oids.certBag).getBytes(),
),
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [certBagAsn1]),
],
);
const safeContentsAsn1 = forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.SEQUENCE,
true,
[safeBagAsn1],
);
const safeContentsDer = forge.asn1.toDer(safeContentsAsn1).getBytes();
const innerContentInfoAsn1 = forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.SEQUENCE,
true,
[
forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.OID,
false,
forge.asn1.oidToDer(forge.pki.oids.data).getBytes(),
),
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING, false, safeContentsDer),
]),
],
);
const authSafeAsn1 = forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.SEQUENCE,
true,
[innerContentInfoAsn1],
);
const authSafeDer = forge.asn1.toDer(authSafeAsn1).getBytes();
const outerContentInfoAsn1 = forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.SEQUENCE,
true,
[
forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.OID,
false,
forge.asn1.oidToDer(forge.pki.oids.data).getBytes(),
),
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING, false, authSafeDer),
]),
],
);
const pfxAsn1 = forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.SEQUENCE,
true,
[
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, String.fromCharCode(3)),
outerContentInfoAsn1,
],
);
const pfxDer = forge.asn1.toDer(pfxAsn1).getBytes();
malformedPfxBuffer = Buffer.from(pfxDer, 'binary');
});
it('Vorbedingung: forge liefert genau einen Bag mit cert=null fuer diese Datei (belegt, dass der Waechter den gemeinten Fall trifft)', () => {
expect(malformedPfxBuffer.length).toBe(83);
const p12Asn1 = forge.asn1.fromDer(
forge.util.createBuffer(malformedPfxBuffer.toString('binary')),
);
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, '');
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
const bags = certBags[forge.pki.oids.certBag] ?? [];
expect(bags).toHaveLength(1);
expect(bags[0].cert).toBeNull();
});
it('parseCert: wirft 400 mit einer Meldung, die den Zertifikats-Bag benennt, statt der irrefuehrenden "Failed to extract certificate details"', async () => {
const service = new CertManagerService();
await expect(
service.parseCert({
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
password: '',
}),
).rejects.toThrow(BadRequestException);
await expect(
service.parseCert({
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
password: '',
}),
).rejects.toThrow(/certificate bag/i);
});
it('mergeCerts (outputFormat pem): wirft 400 mit derselben praezisen Aussage statt "Failed to create merged certificate output"', async () => {
const service = new CertManagerService();
await expect(
service.mergeCerts({
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
outputFormat: 'pem',
password: '',
}),
).rejects.toThrow(BadRequestException);
await expect(
service.mergeCerts({
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
outputFormat: 'pem',
password: '',
}),
).rejects.toThrow(/certificate bag/i);
});
it('mergeCerts (outputFormat pfx): wirft 400 mit derselben praezisen Aussage statt "Failed to create merged certificate output"', async () => {
const service = new CertManagerService();
await expect(
service.mergeCerts({
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
outputFormat: 'pfx',
password: 'secret',
}),
).rejects.toThrow(BadRequestException);
await expect(
service.mergeCerts({
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
outputFormat: 'pfx',
password: 'secret',
}),
).rejects.toThrow(/certificate bag/i);
});
it('convertCert: wirft 400 mit einer Meldung, die den Zertifikats-Bag benennt, statt der irrefuehrenden "Failed to convert certificate to pem: serialization error"', async () => {
const service = new CertManagerService();
await expect(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
service.convertCert({
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
password: '',
targetFormat: 'pem',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any),
).rejects.toThrow(BadRequestException);
await expect(
service.convertCert({
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
password: '',
targetFormat: 'pem',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any),
).rejects.toThrow(/certificate bag/i);
});
it('eine gueltige PFX-Datei verhaelt sich unveraendert: parseCert liefert weiterhin die CertDetails', async () => {
const service = new CertManagerService();
const keys = forge.pki.rsa.generateKeyPair(1024);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [{ name: 'commonName', value: 'valid-pfx.example.com' }];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(keys.privateKey, forge.md.sha256.create());
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(keys.privateKey, [cert], 'secret', {
algorithm: '3des',
});
const validPfxBuffer = Buffer.from(forge.asn1.toDer(p12Asn1).getBytes(), 'binary');
const result = await (service.parseCert({
file: { originalname: 'valid.pfx', buffer: validPfxBuffer },
password: 'secret',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as Promise<any>);
expect(result.subject.cn).toBe('valid-pfx.example.com');
}, 15000);
});