271 lines
7.6 KiB
TypeScript
271 lines
7.6 KiB
TypeScript
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|||
|
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|||
|
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|||
|
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|||
|
import * as plugins from '../../../ts/plugins.js';
|
|||
|
|
|||
|
let testServer: ITestServer;
|
|||
|
|
|||
|
tap.test('setup - start SMTP server with TLS', async () => {
|
|||
|
testServer = await startTestServer({
|
|||
|
port: 2560,
|
|||
|
tlsEnabled: true,
|
|||
|
authRequired: false
|
|||
|
});
|
|||
|
|
|||
|
expect(testServer.port).toEqual(2560);
|
|||
|
expect(testServer.config.tlsEnabled).toBeTrue();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CSEC-01: TLS Verification - should reject invalid certificates by default', async () => {
|
|||
|
let errorCaught = false;
|
|||
|
|
|||
|
try {
|
|||
|
// Create client with strict certificate checking (default)
|
|||
|
const strictClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: true,
|
|||
|
connectionTimeout: 5000,
|
|||
|
tls: {
|
|||
|
rejectUnauthorized: true // Default should be true
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
await strictClient.verify();
|
|||
|
} catch (error: any) {
|
|||
|
errorCaught = true;
|
|||
|
expect(error).toBeInstanceOf(Error);
|
|||
|
// Should fail due to self-signed certificate
|
|||
|
console.log('✅ Self-signed certificate rejected:', error.message);
|
|||
|
}
|
|||
|
|
|||
|
expect(errorCaught).toBeTrue();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CSEC-01: TLS Verification - should accept valid certificates', async () => {
|
|||
|
// For testing, we need to accept self-signed
|
|||
|
const client = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: true,
|
|||
|
connectionTimeout: 5000,
|
|||
|
tls: {
|
|||
|
rejectUnauthorized: false // Accept for testing
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
const isConnected = await client.verify();
|
|||
|
expect(isConnected).toBeTrue();
|
|||
|
|
|||
|
await client.close();
|
|||
|
console.log('✅ Certificate accepted when verification disabled');
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CSEC-01: TLS Verification - should verify hostname matches certificate', async () => {
|
|||
|
let errorCaught = false;
|
|||
|
|
|||
|
try {
|
|||
|
const hostnameClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: true,
|
|||
|
connectionTimeout: 5000,
|
|||
|
tls: {
|
|||
|
rejectUnauthorized: true,
|
|||
|
servername: 'wrong.hostname.com' // Wrong hostname
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
await hostnameClient.verify();
|
|||
|
} catch (error: any) {
|
|||
|
errorCaught = true;
|
|||
|
expect(error).toBeInstanceOf(Error);
|
|||
|
console.log('✅ Hostname mismatch detected:', error.message);
|
|||
|
}
|
|||
|
|
|||
|
expect(errorCaught).toBeTrue();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CSEC-01: TLS Verification - should enforce minimum TLS version', async () => {
|
|||
|
const tlsVersionClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: true,
|
|||
|
connectionTimeout: 5000,
|
|||
|
tls: {
|
|||
|
rejectUnauthorized: false,
|
|||
|
minVersion: 'TLSv1.2', // Enforce minimum version
|
|||
|
maxVersion: 'TLSv1.3'
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
const isConnected = await tlsVersionClient.verify();
|
|||
|
expect(isConnected).toBeTrue();
|
|||
|
|
|||
|
await tlsVersionClient.close();
|
|||
|
console.log('✅ TLS version requirements enforced');
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CSEC-01: TLS Verification - should use strong ciphers only', async () => {
|
|||
|
const cipherClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: true,
|
|||
|
connectionTimeout: 5000,
|
|||
|
tls: {
|
|||
|
rejectUnauthorized: false,
|
|||
|
ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256'
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
const isConnected = await cipherClient.verify();
|
|||
|
expect(isConnected).toBeTrue();
|
|||
|
|
|||
|
await cipherClient.close();
|
|||
|
console.log('✅ Strong cipher suite configuration accepted');
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CSEC-01: TLS Verification - should handle certificate chain validation', async () => {
|
|||
|
// This tests that the client properly validates certificate chains
|
|||
|
const chainClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: true,
|
|||
|
connectionTimeout: 5000,
|
|||
|
tls: {
|
|||
|
rejectUnauthorized: false, // For self-signed test cert
|
|||
|
requestCert: true,
|
|||
|
checkServerIdentity: (hostname, cert) => {
|
|||
|
// Custom validation logic
|
|||
|
console.log('🔍 Validating server certificate:', {
|
|||
|
hostname,
|
|||
|
subject: cert.subject,
|
|||
|
issuer: cert.issuer,
|
|||
|
valid_from: cert.valid_from,
|
|||
|
valid_to: cert.valid_to
|
|||
|
});
|
|||
|
|
|||
|
// Return undefined to indicate success
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
const isConnected = await chainClient.verify();
|
|||
|
expect(isConnected).toBeTrue();
|
|||
|
|
|||
|
await chainClient.close();
|
|||
|
console.log('✅ Certificate chain validation completed');
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CSEC-01: TLS Verification - should detect expired certificates', async () => {
|
|||
|
// For a real test, we'd need an expired certificate
|
|||
|
// This demonstrates the structure for such a test
|
|||
|
|
|||
|
const expiredCertClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: true,
|
|||
|
connectionTimeout: 5000,
|
|||
|
tls: {
|
|||
|
rejectUnauthorized: false,
|
|||
|
checkServerIdentity: (hostname, cert) => {
|
|||
|
// Check if certificate is expired
|
|||
|
const now = new Date();
|
|||
|
const validTo = new Date(cert.valid_to);
|
|||
|
|
|||
|
if (validTo < now) {
|
|||
|
const error = new Error('Certificate has expired');
|
|||
|
(error as any).code = 'CERT_HAS_EXPIRED';
|
|||
|
return error;
|
|||
|
}
|
|||
|
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
const isConnected = await expiredCertClient.verify();
|
|||
|
expect(isConnected).toBeTrue(); // Test cert is not actually expired
|
|||
|
|
|||
|
await expiredCertClient.close();
|
|||
|
console.log('✅ Certificate expiry checking implemented');
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CSEC-01: TLS Verification - should support custom CA certificates', async () => {
|
|||
|
// Read system CA bundle for testing
|
|||
|
let caBundle: string | undefined;
|
|||
|
|
|||
|
try {
|
|||
|
// Common CA bundle locations
|
|||
|
const caPaths = [
|
|||
|
'/etc/ssl/certs/ca-certificates.crt',
|
|||
|
'/etc/ssl/cert.pem',
|
|||
|
'/etc/pki/tls/certs/ca-bundle.crt'
|
|||
|
];
|
|||
|
|
|||
|
for (const path of caPaths) {
|
|||
|
try {
|
|||
|
caBundle = await plugins.fs.promises.readFile(path, 'utf8');
|
|||
|
break;
|
|||
|
} catch {
|
|||
|
continue;
|
|||
|
}
|
|||
|
}
|
|||
|
} catch {
|
|||
|
console.log('ℹ️ Could not load system CA bundle');
|
|||
|
}
|
|||
|
|
|||
|
const caClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: true,
|
|||
|
connectionTimeout: 5000,
|
|||
|
tls: {
|
|||
|
rejectUnauthorized: false, // For self-signed test
|
|||
|
ca: caBundle // Custom CA bundle
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
const isConnected = await caClient.verify();
|
|||
|
expect(isConnected).toBeTrue();
|
|||
|
|
|||
|
await caClient.close();
|
|||
|
console.log('✅ Custom CA certificate support verified');
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CSEC-01: TLS Verification - should protect against downgrade attacks', async () => {
|
|||
|
// Test that client refuses weak TLS versions
|
|||
|
let errorCaught = false;
|
|||
|
|
|||
|
try {
|
|||
|
const weakTlsClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: true,
|
|||
|
connectionTimeout: 5000,
|
|||
|
tls: {
|
|||
|
rejectUnauthorized: false,
|
|||
|
maxVersion: 'TLSv1.0' // Try to force old TLS
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
await weakTlsClient.verify();
|
|||
|
|
|||
|
// If server accepts TLSv1.0, that's a concern
|
|||
|
console.log('⚠️ Server accepted TLSv1.0 - consider requiring TLSv1.2+');
|
|||
|
} catch (error) {
|
|||
|
errorCaught = true;
|
|||
|
console.log('✅ Weak TLS version rejected');
|
|||
|
}
|
|||
|
|
|||
|
// Either rejection or warning is acceptable for this test
|
|||
|
expect(true).toBeTrue();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('cleanup - stop SMTP server', async () => {
|
|||
|
await stopTestServer(testServer);
|
|||
|
});
|
|||
|
|
|||
|
tap.start();
|