/** * DC-147: CA naming + name constraint + backup hardening tests. * * Hermetic where possible: real openssl runs in tmp dirs (available in the * test env); the whole openssl-dependent suite is gated behind a capability * probe so CI without openssl skips cleanly. Route-level tests for * POST /api/ca/backup drive the real router with mocked platform paths. */ const fs = require('fs'); const os = require('os'); const path = require('path'); const { execFileSync } = require('child_process'); const { opensslAvailable, setupNamedRootCA, verifyNameConstraint, assertLeafCompliance, backupRootKey, restoreRootKeyBackup, validateCommonName, normalizeTld } = require('../src/network/ca-setup'); const HAS_OPENSSL = opensslAvailable(); const OPENSSL_GATED = HAS_OPENSSL ? describe : describe.skip; let tmpDir; const CN = 'Ahmed Family Certificate Authority'; beforeAll(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dc147-ca-')); }); afterAll(() => { if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** Generate a leaf signed by `root` with configurable extensions/days. */ function makeLeaf(root, name, extContent, days, cn = 'app.home') { const key = path.join(tmpDir, `${name}.key`); const csr = path.join(tmpDir, `${name}.csr`); const crt = path.join(tmpDir, `${name}.crt`); execFileSync('openssl', ['genrsa', '-out', key, '2048'], { stdio: 'pipe' }); execFileSync('openssl', ['req', '-new', '-key', key, '-out', csr, '-subj', `/CN=${cn}`], { stdio: 'pipe' }); const extArgs = extContent ? ['-extfile', (() => { const f = path.join(tmpDir, `${name}.cnf`); fs.writeFileSync(f, extContent); return f; })()] : []; execFileSync('openssl', [ 'x509', '-req', '-in', csr, '-CA', root.rootCertPath, '-CAkey', root.rootKeyPath, '-CAcreateserial', '-out', crt, '-days', String(days), '-sha256', ...extArgs ], { stdio: 'pipe' }); return crt; } describe('DC-147 input validation (hermetic)', () => { test('validateCommonName accepts friendly names, rejects injection', () => { expect(validateCommonName('Ahmed Family Certificate Authority')).toBe('Ahmed Family Certificate Authority'); expect(validateCommonName(' padded ')).toBe('padded'); expect(() => validateCommonName('')).toThrow(); expect(() => validateCommonName(null)).toThrow(); expect(() => validateCommonName('x'.repeat(65))).toThrow(); expect(() => validateCommonName('Bad;/CN=evil')).toThrow(); expect(() => validateCommonName('New\nline')).toThrow(); }); test('normalizeTld strips dots and rejects bad labels', () => { expect(normalizeTld('.home')).toBe('home'); expect(normalizeTld('home')).toBe('home'); expect(normalizeTld('.HOME')).toBe('home'); expect(() => normalizeTld('')).toThrow(); expect(() => normalizeTld('.')).toThrow(); expect(() => normalizeTld('two.words')).toThrow(); expect(() => normalizeTld('bad/tld')).toThrow(); }); }); OPENSSL_GATED('DC-147 setupNamedRootCA (real openssl, tmp dir)', () => { test('generated root has critical nameConstraints pinned to the TLD (x509 -text)', async () => { const r = await setupNamedRootCA({ commonName: CN, privateTld: '.home', dir: tmpDir }); // Raw openssl parse — independent of verifyNameConstraint. const text = execFileSync('openssl', ['x509', '-in', r.rootCertPath, '-text', '-noout']).toString(); expect(text).toMatch(/X509v3 Name Constraints:\s+critical/); expect(text).toMatch(/Permitted:\s*\n\s+DNS:\.\s*home/); expect(text).toContain(`CN = ${CN}`); expect(text).toMatch(/CA:TRUE/); // cert-info.json mirrors the existing flow's shape. const info = JSON.parse(fs.readFileSync(path.join(tmpDir, 'cert-info.json'), 'utf8')); expect(info.name).toBe(CN); expect(info.fingerprint).toMatch(/^[0-9A-F:]{95}$/); expect(info.serialNumber).toMatch(/^[0-9A-F]+$/); expect(info.validFrom).toBeTruthy(); expect(info.validUntil).toBeTruthy(); expect(info.nameConstraints).toEqual({ critical: true, permitted: ['.home'] }); // Root key perms tight. expect((fs.statSync(r.rootKeyPath).mode & 0o777)).toBe(0o600); // Root can only sign names under .home — a leaf for another TLD must fail // path validation (name constraint enforced by openssl verify). const foreignLeaf = makeLeaf(r, 'foreign', null, 30, 'app.example.com'); let verifyOk = true; try { execFileSync('openssl', ['verify', '-CAfile', r.rootCertPath, foreignLeaf], { stdio: 'pipe' }); } catch { verifyOk = false; } expect(verifyOk).toBe(false); }, 30000); test('second setup attempt without force REFUSES', async () => { const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'dc147-ca2-')); try { await setupNamedRootCA({ commonName: CN, privateTld: '.home', dir: dir2 }); await expect(setupNamedRootCA({ commonName: 'Renamed CA', privateTld: '.home', dir: dir2 })) .rejects.toMatchObject({ code: 'DC147_ROOT_EXISTS' }); // force without the exact confirmName still refuses. await expect(setupNamedRootCA({ commonName: 'Renamed CA', privateTld: '.home', dir: dir2, force: true, confirmName: 'Wrong Name' })) .rejects.toMatchObject({ code: 'DC147_ROOT_EXISTS' }); // force + correct confirmName proceeds and archives the old root. const r2 = await setupNamedRootCA({ commonName: 'Renamed CA', privateTld: '.home', dir: dir2, force: true, confirmName: CN }); expect(r2.certInfo.name).toBe('Renamed CA'); expect(fs.readdirSync(dir2).some((f) => f.startsWith('root.crt.replaced-'))).toBe(true); } finally { fs.rmSync(dir2, { recursive: true, force: true }); } }, 30000); }); OPENSSL_GATED('DC-147 verifyNameConstraint', () => { test('true for the pinned TLD, false for others / non-constrained certs', async () => { const r = await setupNamedRootCA({ commonName: CN, privateTld: '.home', dir: path.join(tmpDir, 'nc') }); expect(verifyNameConstraint(r.rootCertPath, '.home')).toBe(true); expect(verifyNameConstraint(r.rootCertPath, 'home')).toBe(true); expect(verifyNameConstraint(r.rootCertPath, '.lan')).toBe(false); expect(verifyNameConstraint(r.rootCertPath, 'hom')).toBe(false); // A plain self-signed cert with no nameConstraints. const plainKey = path.join(tmpDir, 'plain.key'); const plainCrt = path.join(tmpDir, 'plain.crt'); execFileSync('openssl', ['genrsa', '-out', plainKey, '2048'], { stdio: 'pipe' }); execFileSync('openssl', ['req', '-x509', '-new', '-key', plainKey, '-subj', '/CN=plain', '-days', '30', '-out', plainCrt], { stdio: 'pipe' }); expect(verifyNameConstraint(plainCrt, 'home')).toBe(false); expect(verifyNameConstraint(path.join(tmpDir, 'no-such-cert.crt'), 'home')).toBe(false); }, 30000); }); OPENSSL_GATED('DC-147 assertLeafCompliance', () => { test('passes for a compliant leaf (SAN + serverAuth + ≤398 days)', async () => { const root = await setupNamedRootCA({ commonName: CN, privateTld: '.home', dir: path.join(tmpDir, 'leaf-ok') }); const leaf = makeLeaf(root, 'good-leaf', 'subjectAltName=DNS:app.home\nextendedKeyUsage=serverAuth\n', 365); const result = assertLeafCompliance(leaf); expect(result.compliant).toBe(true); expect(result.violations).toEqual([]); }, 30000); test('FAILS for a 720-day no-SAN leaf (deliberately non-compliant)', async () => { const root = await setupNamedRootCA({ commonName: CN, privateTld: '.home', dir: path.join(tmpDir, 'leaf-bad') }); const leaf = makeLeaf(root, 'bad-leaf', null, 720); const result = assertLeafCompliance(leaf); expect(result.compliant).toBe(false); expect(result.violations.join(' ')).toMatch(/subjectAltName/); expect(result.violations.join(' ')).toMatch(/serverAuth/); expect(result.violations.join(' ')).toMatch(/398/); }, 30000); test('FAILS for a long-validity leaf that otherwise has SAN + EKU', async () => { const root = await setupNamedRootCA({ commonName: CN, privateTld: '.home', dir: path.join(tmpDir, 'leaf-long') }); const leaf = makeLeaf(root, 'long-leaf', 'subjectAltName=DNS:app.home\nextendedKeyUsage=serverAuth\n', 500); const result = assertLeafCompliance(leaf); expect(result.compliant).toBe(false); expect(result.violations).toHaveLength(1); expect(result.violations[0]).toMatch(/398/); }, 30000); }); OPENSSL_GATED('DC-147 encrypted root-key backup', () => { test('backupRootKey returns ciphertext + checksum; round-trip decrypt proves the passphrase works', async () => { const dir = path.join(tmpDir, 'backup'); const root = await setupNamedRootCA({ commonName: CN, privateTld: '.home', dir }); const plaintextKey = fs.readFileSync(root.rootKeyPath, 'utf8'); expect(plaintextKey).toContain('PRIVATE KEY'); const passphrase = 'correct horse battery staple'; const backup = backupRootKey({ dir, passphrase }); // Ciphertext never contains the plaintext key marker. expect(typeof backup.ciphertext).toBe('string'); expect(backup.ciphertext).not.toContain('PRIVATE KEY'); expect(backup.ciphertext.length).toBeGreaterThan(0); // sha256 checksum of the ciphertext. expect(backup.sha256).toMatch(/^[a-f0-9]{64}$/); const crypto = require('crypto'); expect(crypto.createHash('sha256').update(backup.ciphertext, 'utf8').digest('hex')).toBe(backup.sha256); // The key itself does not leak anywhere in the payload. expect(backup.ciphertext).not.toContain(plaintextKey.slice(0, 40)); // Round-trip: decrypting with the passphrase reproduces the exact key. const restored = restoreRootKeyBackup({ ciphertext: backup.ciphertext, passphrase }).toString('utf8'); expect(restored).toBe(plaintextKey); // Wrong passphrase must fail. let wrongOk = true; try { restoreRootKeyBackup({ ciphertext: backup.ciphertext, passphrase: 'wrong passphrase!' }); } catch { wrongOk = false; } expect(wrongOk).toBe(false); }, 30000); test('rejects short/missing passphrase and missing root key', () => { expect(() => backupRootKey({ dir: tmpDir, passphrase: 'short' })).toThrow(/8 characters/); expect(() => backupRootKey({ dir: tmpDir, passphrase: undefined })).toThrow(); let missingErr; try { backupRootKey({ dir: path.join(tmpDir, 'definitely-empty'), passphrase: 'long-enough-pass' }); } catch (e) { missingErr = e; } expect(missingErr).toMatchObject({ code: 'DC147_ROOT_KEY_MISSING' }); }); }); if (!HAS_OPENSSL) { test('openssl capability notice', () => { // Gated: this environment lacks a working openssl binary; the real-cert // blocks above are skipped rather than failing. expect(true).toBe(true); }); }