507 lines
17 KiB
TypeScript
507 lines
17 KiB
TypeScript
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
import * as plugins from './plugins.js';
|
||
|
import { createTestServer } from '../../helpers/server.loader.js';
|
||
|
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||
|
|
||
|
tap.test('CSEC-07: should handle cipher suites correctly', async (tools) => {
|
||
|
const testId = 'CSEC-07-cipher-suites';
|
||
|
console.log(`\n${testId}: Testing cipher suite handling...`);
|
||
|
|
||
|
let scenarioCount = 0;
|
||
|
|
||
|
// Scenario 1: Strong cipher suite negotiation
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing strong cipher suite negotiation`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
secure: true,
|
||
|
tlsOptions: {
|
||
|
// Configure strong ciphers only
|
||
|
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES256-GCM-SHA384',
|
||
|
honorCipherOrder: true,
|
||
|
minVersion: 'TLSv1.2'
|
||
|
},
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected with strong ciphers');
|
||
|
|
||
|
// Log cipher info if available
|
||
|
const tlsSocket = socket as any;
|
||
|
if (tlsSocket.getCipher) {
|
||
|
const cipher = tlsSocket.getCipher();
|
||
|
console.log(` [Server] Negotiated cipher: ${cipher.name} (${cipher.version})`);
|
||
|
}
|
||
|
|
||
|
socket.write('220 secure.example.com ESMTP\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
console.log(` [Server] Received: ${command}`);
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-secure.example.com\r\n');
|
||
|
socket.write('250-SIZE 10485760\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'DATA') {
|
||
|
socket.write('354 Start mail input\r\n');
|
||
|
} else if (command === '.') {
|
||
|
socket.write('250 OK: Message accepted with strong encryption\r\n');
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221 Bye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: true,
|
||
|
tls: {
|
||
|
rejectUnauthorized: false,
|
||
|
// Prefer strong ciphers
|
||
|
ciphers: 'HIGH:!aNULL:!MD5:!3DES',
|
||
|
minVersion: 'TLSv1.2'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: 'Strong cipher test',
|
||
|
text: 'Testing with strong cipher suites'
|
||
|
});
|
||
|
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
console.log(' Successfully negotiated strong cipher');
|
||
|
expect(result).toBeDefined();
|
||
|
expect(result.messageId).toBeDefined();
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 2: Weak cipher rejection
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing weak cipher rejection`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
secure: true,
|
||
|
tlsOptions: {
|
||
|
// Only allow strong ciphers
|
||
|
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256',
|
||
|
honorCipherOrder: true,
|
||
|
minVersion: 'TLSv1.2'
|
||
|
},
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected');
|
||
|
socket.write('220 secure.example.com ESMTP\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-secure.example.com\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'DATA') {
|
||
|
socket.write('354 Start mail input\r\n');
|
||
|
} else if (command === '.') {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221 Bye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Try to connect with weak ciphers only (should fail)
|
||
|
const weakClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: true,
|
||
|
tls: {
|
||
|
rejectUnauthorized: false,
|
||
|
// Try to use weak ciphers
|
||
|
ciphers: 'DES-CBC3-SHA:RC4-SHA',
|
||
|
maxVersion: 'TLSv1.0'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: 'Weak cipher test',
|
||
|
text: 'Testing weak cipher rejection'
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
await weakClient.sendMail(email);
|
||
|
console.log(' Unexpected: Weak cipher was accepted');
|
||
|
} catch (error) {
|
||
|
console.log(` Expected error: ${error.message}`);
|
||
|
expect(error.message).toMatch(/handshake|cipher|ssl/i);
|
||
|
}
|
||
|
|
||
|
// Connect with acceptable ciphers
|
||
|
const strongClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: true,
|
||
|
tls: {
|
||
|
rejectUnauthorized: false,
|
||
|
ciphers: 'HIGH:!aNULL',
|
||
|
minVersion: 'TLSv1.2'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const result = await strongClient.sendMail(email);
|
||
|
console.log(' Successfully connected with strong ciphers');
|
||
|
expect(result).toBeDefined();
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 3: Cipher suite priority testing
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing cipher suite priority`);
|
||
|
|
||
|
const preferredCiphers = [
|
||
|
'TLS_AES_256_GCM_SHA384',
|
||
|
'TLS_AES_128_GCM_SHA256',
|
||
|
'ECDHE-RSA-AES256-GCM-SHA384',
|
||
|
'ECDHE-RSA-AES128-GCM-SHA256'
|
||
|
];
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
secure: true,
|
||
|
tlsOptions: {
|
||
|
ciphers: preferredCiphers.join(':'),
|
||
|
honorCipherOrder: true, // Server chooses cipher
|
||
|
minVersion: 'TLSv1.2'
|
||
|
},
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected');
|
||
|
|
||
|
const tlsSocket = socket as any;
|
||
|
if (tlsSocket.getCipher) {
|
||
|
const cipher = tlsSocket.getCipher();
|
||
|
console.log(` [Server] Selected cipher: ${cipher.name}`);
|
||
|
|
||
|
// Check if preferred cipher was selected
|
||
|
const cipherIndex = preferredCiphers.findIndex(c =>
|
||
|
cipher.name.includes(c) || c.includes(cipher.name)
|
||
|
);
|
||
|
console.log(` [Server] Cipher priority: ${cipherIndex + 1}/${preferredCiphers.length}`);
|
||
|
}
|
||
|
|
||
|
socket.write('220 priority.example.com ESMTP\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-priority.example.com\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'DATA') {
|
||
|
socket.write('354 Start mail input\r\n');
|
||
|
} else if (command === '.') {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221 Bye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: true,
|
||
|
tls: {
|
||
|
rejectUnauthorized: false,
|
||
|
// Client offers ciphers in different order
|
||
|
ciphers: preferredCiphers.slice().reverse().join(':'),
|
||
|
honorCipherOrder: false // Let server choose
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: 'Cipher priority test',
|
||
|
text: 'Testing cipher suite selection priority'
|
||
|
});
|
||
|
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
console.log(' Cipher negotiation completed');
|
||
|
expect(result).toBeDefined();
|
||
|
expect(result.messageId).toBeDefined();
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 4: Perfect Forward Secrecy (PFS) ciphers
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing Perfect Forward Secrecy ciphers`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
secure: true,
|
||
|
tlsOptions: {
|
||
|
// Only PFS ciphers (ECDHE/DHE)
|
||
|
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384',
|
||
|
honorCipherOrder: true,
|
||
|
ecdhCurve: 'auto'
|
||
|
},
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected with PFS');
|
||
|
|
||
|
const tlsSocket = socket as any;
|
||
|
if (tlsSocket.getCipher) {
|
||
|
const cipher = tlsSocket.getCipher();
|
||
|
const hasPFS = cipher.name.includes('ECDHE') || cipher.name.includes('DHE');
|
||
|
console.log(` [Server] Cipher: ${cipher.name} (PFS: ${hasPFS ? 'Yes' : 'No'})`);
|
||
|
}
|
||
|
|
||
|
socket.write('220 pfs.example.com ESMTP\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-pfs.example.com\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'DATA') {
|
||
|
socket.write('354 Start mail input\r\n');
|
||
|
} else if (command === '.') {
|
||
|
socket.write('250 OK: Message sent with PFS\r\n');
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221 Bye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: true,
|
||
|
tls: {
|
||
|
rejectUnauthorized: false,
|
||
|
// Prefer PFS ciphers
|
||
|
ciphers: 'ECDHE:DHE:!aNULL:!MD5',
|
||
|
ecdhCurve: 'auto'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: 'PFS cipher test',
|
||
|
text: 'Testing Perfect Forward Secrecy'
|
||
|
});
|
||
|
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
console.log(' Successfully used PFS cipher');
|
||
|
expect(result).toBeDefined();
|
||
|
expect(result.messageId).toBeDefined();
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 5: Cipher renegotiation
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing cipher renegotiation handling`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
secure: true,
|
||
|
tlsOptions: {
|
||
|
ciphers: 'HIGH:MEDIUM:!aNULL:!MD5',
|
||
|
// Disable renegotiation for security
|
||
|
secureOptions: plugins.crypto.constants.SSL_OP_NO_RENEGOTIATION
|
||
|
},
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected');
|
||
|
socket.write('220 norenegotiation.example.com ESMTP\r\n');
|
||
|
|
||
|
let messageCount = 0;
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-norenegotiation.example.com\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
messageCount++;
|
||
|
console.log(` [Server] Processing message ${messageCount}`);
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'DATA') {
|
||
|
socket.write('354 Start mail input\r\n');
|
||
|
} else if (command === '.') {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'RSET') {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221 Bye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: true,
|
||
|
tls: {
|
||
|
rejectUnauthorized: false,
|
||
|
// Also disable renegotiation on client
|
||
|
secureOptions: plugins.crypto.constants.SSL_OP_NO_RENEGOTIATION
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Send multiple emails on same connection
|
||
|
for (let i = 0; i < 3; i++) {
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: `Renegotiation test ${i + 1}`,
|
||
|
text: `Testing without cipher renegotiation - email ${i + 1}`
|
||
|
});
|
||
|
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
console.log(` Email ${i + 1} sent without renegotiation`);
|
||
|
expect(result).toBeDefined();
|
||
|
expect(result.messageId).toBeDefined();
|
||
|
}
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 6: Cipher compatibility testing
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing cipher compatibility`);
|
||
|
|
||
|
const cipherSets = [
|
||
|
{
|
||
|
name: 'TLS 1.3 only',
|
||
|
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256',
|
||
|
minVersion: 'TLSv1.3',
|
||
|
maxVersion: 'TLSv1.3'
|
||
|
},
|
||
|
{
|
||
|
name: 'TLS 1.2 compatible',
|
||
|
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256',
|
||
|
minVersion: 'TLSv1.2',
|
||
|
maxVersion: 'TLSv1.2'
|
||
|
},
|
||
|
{
|
||
|
name: 'Broad compatibility',
|
||
|
ciphers: 'HIGH:MEDIUM:!aNULL:!MD5:!3DES',
|
||
|
minVersion: 'TLSv1.2',
|
||
|
maxVersion: undefined
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const cipherSet of cipherSets) {
|
||
|
console.log(`\n Testing ${cipherSet.name}...`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
secure: true,
|
||
|
tlsOptions: {
|
||
|
ciphers: cipherSet.ciphers,
|
||
|
minVersion: cipherSet.minVersion as any,
|
||
|
maxVersion: cipherSet.maxVersion as any
|
||
|
},
|
||
|
onConnection: async (socket) => {
|
||
|
const tlsSocket = socket as any;
|
||
|
if (tlsSocket.getCipher && tlsSocket.getProtocol) {
|
||
|
const cipher = tlsSocket.getCipher();
|
||
|
const protocol = tlsSocket.getProtocol();
|
||
|
console.log(` [Server] Protocol: ${protocol}, Cipher: ${cipher.name}`);
|
||
|
}
|
||
|
|
||
|
socket.write('220 compat.example.com ESMTP\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-compat.example.com\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'DATA') {
|
||
|
socket.write('354 Start mail input\r\n');
|
||
|
} else if (command === '.') {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221 Bye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: true,
|
||
|
tls: {
|
||
|
rejectUnauthorized: false,
|
||
|
ciphers: cipherSet.ciphers,
|
||
|
minVersion: cipherSet.minVersion as any,
|
||
|
maxVersion: cipherSet.maxVersion as any
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: `${cipherSet.name} test`,
|
||
|
text: `Testing ${cipherSet.name} cipher configuration`
|
||
|
});
|
||
|
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
console.log(` Success with ${cipherSet.name}`);
|
||
|
expect(result).toBeDefined();
|
||
|
expect(result.messageId).toBeDefined();
|
||
|
} catch (error) {
|
||
|
console.log(` ${cipherSet.name} not supported in this environment`);
|
||
|
}
|
||
|
|
||
|
await testServer.server.close();
|
||
|
}
|
||
|
})();
|
||
|
|
||
|
console.log(`\n${testId}: All ${scenarioCount} cipher suite scenarios tested ✓`);
|
||
|
});
|