import { tap, expect } from '@git.zone/tstest/tapbundle'; import { startTestSmtpServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../helpers/smtp.client.js'; import { Email } from '../../../ts/mail/core/classes.email.js'; import * as crypto from 'crypto'; let testServer: any; tap.test('setup test SMTP server', async () => { testServer = await startTestSmtpServer(); expect(testServer).toBeTruthy(); expect(testServer.port).toBeGreaterThan(0); }); tap.test('CSEC-03: Basic DKIM signature structure', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Create email with DKIM configuration const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'DKIM Signed Email', text: 'This email should be DKIM signed', dkim: { domainName: 'example.com', keySelector: 'default', privateKey: '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3...\n-----END PRIVATE KEY-----', canonicalization: 'relaxed/relaxed' } }); // Monitor for DKIM-Signature header let dkimSignature = ''; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.toLowerCase().includes('dkim-signature:')) { dkimSignature = command; } return originalSendCommand(command); }; const result = await smtpClient.sendMail(email); expect(result).toBeTruthy(); if (dkimSignature) { console.log('DKIM-Signature header found:'); console.log(dkimSignature.substring(0, 100) + '...'); // Parse DKIM signature components const components = dkimSignature.match(/(\w+)=([^;]+)/g); if (components) { console.log('\nDKIM components:'); components.forEach(comp => { const [key, value] = comp.split('='); console.log(` ${key}: ${value.trim().substring(0, 50)}${value.length > 50 ? '...' : ''}`); }); } } else { console.log('DKIM signing not implemented in Email class'); } await smtpClient.close(); }); tap.test('CSEC-03: DKIM canonicalization methods', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test different canonicalization methods const canonicalizations = [ 'simple/simple', 'simple/relaxed', 'relaxed/simple', 'relaxed/relaxed' ]; for (const canon of canonicalizations) { console.log(`\nTesting canonicalization: ${canon}`); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: `DKIM Canon Test: ${canon}`, text: 'Testing canonicalization\r\n with various spaces\r\n\r\nand blank lines.\r\n', headers: { 'X-Test-Header': ' value with spaces ' }, dkim: { domainName: 'example.com', keySelector: 'test', privateKey: 'mock-key', canonicalization: canon } }); try { const result = await smtpClient.sendMail(email); console.log(` Result: ${result ? 'Success' : 'Failed'}`); } catch (error) { console.log(` Error: ${error.message}`); } } await smtpClient.close(); }); tap.test('CSEC-03: DKIM header selection', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test header selection for DKIM signing const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], cc: ['cc@example.com'], subject: 'DKIM Header Selection Test', text: 'Testing which headers are included in DKIM signature', headers: { 'X-Priority': 'High', 'X-Mailer': 'Test Client', 'List-Unsubscribe': '' }, dkim: { domainName: 'example.com', keySelector: 'default', privateKey: 'mock-key', headerFieldNames: [ 'From', 'To', 'Subject', 'Date', 'Message-ID', 'X-Priority', 'List-Unsubscribe' ] } }); // Monitor signed headers let signedHeaders: string[] = []; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.toLowerCase().includes('dkim-signature:')) { const hMatch = command.match(/h=([^;]+)/); if (hMatch) { signedHeaders = hMatch[1].split(':').map(h => h.trim()); } } return originalSendCommand(command); }; const result = await smtpClient.sendMail(email); if (signedHeaders.length > 0) { console.log('\nHeaders included in DKIM signature:'); signedHeaders.forEach(h => console.log(` - ${h}`)); // Check if important headers are included const importantHeaders = ['from', 'to', 'subject', 'date']; const missingHeaders = importantHeaders.filter(h => !signedHeaders.some(sh => sh.toLowerCase() === h) ); if (missingHeaders.length > 0) { console.log('\nWARNING: Important headers missing from signature:'); missingHeaders.forEach(h => console.log(` - ${h}`)); } } await smtpClient.close(); }); tap.test('CSEC-03: DKIM with RSA key generation', async () => { // Generate a test RSA key pair const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); console.log('Generated RSA key pair for DKIM:'); console.log('Public key (first line):', publicKey.split('\n')[1].substring(0, 50) + '...'); // Create DNS TXT record format const publicKeyBase64 = publicKey .replace(/-----BEGIN PUBLIC KEY-----/, '') .replace(/-----END PUBLIC KEY-----/, '') .replace(/\s/g, ''); console.log('\nDNS TXT record for default._domainkey.example.com:'); console.log(`v=DKIM1; k=rsa; p=${publicKeyBase64.substring(0, 50)}...`); const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'DKIM with Real RSA Key', text: 'This email is signed with a real RSA key', dkim: { domainName: 'example.com', keySelector: 'default', privateKey: privateKey, hashAlgo: 'sha256' } }); const result = await smtpClient.sendMail(email); expect(result).toBeTruthy(); await smtpClient.close(); }); tap.test('CSEC-03: DKIM body hash calculation', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test body hash with different content const testBodies = [ { name: 'Simple text', body: 'Hello World' }, { name: 'Multi-line text', body: 'Line 1\r\nLine 2\r\nLine 3' }, { name: 'Trailing newlines', body: 'Content\r\n\r\n\r\n' }, { name: 'Empty body', body: '' } ]; for (const test of testBodies) { console.log(`\nTesting body hash for: ${test.name}`); // Calculate expected body hash const canonicalBody = test.body.replace(/\r\n/g, '\n').trimEnd() + '\n'; const bodyHash = crypto.createHash('sha256').update(canonicalBody).digest('base64'); console.log(` Expected hash: ${bodyHash.substring(0, 20)}...`); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: `Body Hash Test: ${test.name}`, text: test.body, dkim: { domainName: 'example.com', keySelector: 'default', privateKey: 'mock-key' } }); // Monitor for body hash in DKIM signature let capturedBodyHash = ''; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.toLowerCase().includes('dkim-signature:')) { const bhMatch = command.match(/bh=([^;]+)/); if (bhMatch) { capturedBodyHash = bhMatch[1].trim(); } } return originalSendCommand(command); }; await smtpClient.sendMail(email); if (capturedBodyHash) { console.log(` Actual hash: ${capturedBodyHash.substring(0, 20)}...`); } } await smtpClient.close(); }); tap.test('CSEC-03: DKIM multiple signatures', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Email with multiple DKIM signatures (e.g., author + ESP) const email = new Email({ from: 'sender@author-domain.com', to: ['recipient@example.com'], subject: 'Multiple DKIM Signatures', text: 'This email has multiple DKIM signatures', dkim: [ { domainName: 'author-domain.com', keySelector: 'default', privateKey: 'author-key' }, { domainName: 'esp-domain.com', keySelector: 'esp2024', privateKey: 'esp-key' } ] }); // Count DKIM signatures let dkimCount = 0; const signatures: string[] = []; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.toLowerCase().includes('dkim-signature:')) { dkimCount++; signatures.push(command); } return originalSendCommand(command); }; const result = await smtpClient.sendMail(email); console.log(`\nDKIM signatures found: ${dkimCount}`); signatures.forEach((sig, i) => { const domainMatch = sig.match(/d=([^;]+)/); const selectorMatch = sig.match(/s=([^;]+)/); console.log(`Signature ${i + 1}:`); console.log(` Domain: ${domainMatch ? domainMatch[1] : 'unknown'}`); console.log(` Selector: ${selectorMatch ? selectorMatch[1] : 'unknown'}`); }); await smtpClient.close(); }); tap.test('CSEC-03: DKIM timestamp and expiration', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test DKIM with timestamp and expiration const now = Math.floor(Date.now() / 1000); const oneHourLater = now + 3600; const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'DKIM with Timestamp', text: 'This signature expires in one hour', dkim: { domainName: 'example.com', keySelector: 'default', privateKey: 'mock-key', signTime: now, expireTime: oneHourLater } }); // Monitor for timestamp fields let hasTimestamp = false; let hasExpiration = false; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.toLowerCase().includes('dkim-signature:')) { if (command.includes('t=')) hasTimestamp = true; if (command.includes('x=')) hasExpiration = true; const tMatch = command.match(/t=(\d+)/); const xMatch = command.match(/x=(\d+)/); if (tMatch) console.log(` Signature time: ${new Date(parseInt(tMatch[1]) * 1000).toISOString()}`); if (xMatch) console.log(` Expiration time: ${new Date(parseInt(xMatch[1]) * 1000).toISOString()}`); } return originalSendCommand(command); }; await smtpClient.sendMail(email); console.log(`\nDKIM timestamp included: ${hasTimestamp}`); console.log(`DKIM expiration included: ${hasExpiration}`); await smtpClient.close(); }); tap.test('CSEC-03: DKIM failure scenarios', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test various DKIM failure scenarios const failureTests = [ { name: 'Missing private key', dkim: { domainName: 'example.com', keySelector: 'default', privateKey: undefined } }, { name: 'Invalid domain', dkim: { domainName: '', keySelector: 'default', privateKey: 'key' } }, { name: 'Missing selector', dkim: { domainName: 'example.com', keySelector: '', privateKey: 'key' } }, { name: 'Invalid algorithm', dkim: { domainName: 'example.com', keySelector: 'default', privateKey: 'key', hashAlgo: 'md5' // Should not be allowed } } ]; for (const test of failureTests) { console.log(`\nTesting DKIM failure: ${test.name}`); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: `DKIM Failure Test: ${test.name}`, text: 'Testing DKIM failure scenario', dkim: test.dkim as any }); try { const result = await smtpClient.sendMail(email); console.log(` Result: Email sent ${result ? 'successfully' : 'with issues'}`); console.log(` Note: DKIM might be skipped or handled gracefully`); } catch (error) { console.log(` Error (expected): ${error.message}`); } } await smtpClient.close(); }); tap.test('CSEC-03: DKIM performance impact', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 10000, debug: false // Quiet for performance test }); await smtpClient.connect(); // Test performance with and without DKIM const iterations = 10; const bodySizes = [100, 1000, 10000]; // bytes for (const size of bodySizes) { const body = 'x'.repeat(size); // Without DKIM const withoutDkimTimes: number[] = []; for (let i = 0; i < iterations; i++) { const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Performance Test', text: body }); const start = Date.now(); await smtpClient.sendMail(email); withoutDkimTimes.push(Date.now() - start); } // With DKIM const withDkimTimes: number[] = []; for (let i = 0; i < iterations; i++) { const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Performance Test', text: body, dkim: { domainName: 'example.com', keySelector: 'default', privateKey: 'mock-key' } }); const start = Date.now(); await smtpClient.sendMail(email); withDkimTimes.push(Date.now() - start); } const avgWithout = withoutDkimTimes.reduce((a, b) => a + b) / iterations; const avgWith = withDkimTimes.reduce((a, b) => a + b) / iterations; const overhead = ((avgWith - avgWithout) / avgWithout) * 100; console.log(`\nBody size: ${size} bytes`); console.log(` Without DKIM: ${avgWithout.toFixed(2)}ms avg`); console.log(` With DKIM: ${avgWith.toFixed(2)}ms avg`); console.log(` Overhead: ${overhead.toFixed(1)}%`); } await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();