2025-05-21 12:52:24 +00:00
|
|
|
/**
|
|
|
|
* SMTP TLS Handler
|
|
|
|
* Responsible for handling TLS-related SMTP functionality
|
|
|
|
*/
|
|
|
|
|
|
|
|
import * as plugins from '../../../plugins.js';
|
2025-05-21 13:42:12 +00:00
|
|
|
import type { ITlsHandler, ISessionManager } from './interfaces.js';
|
2025-05-21 12:52:24 +00:00
|
|
|
import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js';
|
|
|
|
import { SmtpLogger } from './utils/logging.js';
|
|
|
|
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
|
2025-05-21 14:28:33 +00:00
|
|
|
import { SmtpState } from '../interfaces.js';
|
2025-05-21 12:52:24 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles TLS functionality for SMTP server
|
|
|
|
*/
|
|
|
|
export class TlsHandler implements ITlsHandler {
|
|
|
|
/**
|
|
|
|
* Session manager instance
|
|
|
|
*/
|
|
|
|
private sessionManager: ISessionManager;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* TLS options
|
|
|
|
*/
|
|
|
|
private options: {
|
|
|
|
key: string;
|
|
|
|
cert: string;
|
|
|
|
ca?: string;
|
|
|
|
rejectUnauthorized?: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new TLS handler
|
|
|
|
* @param sessionManager - Session manager instance
|
|
|
|
* @param options - TLS options
|
|
|
|
*/
|
|
|
|
constructor(
|
|
|
|
sessionManager: ISessionManager,
|
|
|
|
options: {
|
|
|
|
key: string;
|
|
|
|
cert: string;
|
|
|
|
ca?: string;
|
|
|
|
rejectUnauthorized?: boolean;
|
|
|
|
}
|
|
|
|
) {
|
|
|
|
this.sessionManager = sessionManager;
|
|
|
|
this.options = options;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle STARTTLS command
|
|
|
|
* @param socket - Client socket
|
|
|
|
*/
|
|
|
|
public handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
|
|
|
// Get the session for this socket
|
|
|
|
const session = this.sessionManager.getSession(socket);
|
|
|
|
if (!session) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if already using TLS
|
|
|
|
if (session.useTLS) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if we have the necessary TLS certificates
|
|
|
|
if (!this.isTlsEnabled()) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send ready for TLS response
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_READY} Ready to start TLS`);
|
|
|
|
|
|
|
|
// Upgrade the connection to TLS
|
|
|
|
try {
|
|
|
|
this.startTLS(socket);
|
|
|
|
} catch (error) {
|
|
|
|
SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
|
|
sessionId: session.id,
|
|
|
|
remoteAddress: session.remoteAddress,
|
|
|
|
error: error instanceof Error ? error : new Error(String(error))
|
|
|
|
});
|
|
|
|
|
|
|
|
// Log security event
|
|
|
|
SmtpLogger.logSecurityEvent(
|
|
|
|
SecurityLogLevel.ERROR,
|
|
|
|
SecurityEventType.TLS_NEGOTIATION,
|
|
|
|
'STARTTLS negotiation failed',
|
|
|
|
{ error: error instanceof Error ? error.message : String(error) },
|
|
|
|
session.remoteAddress
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Upgrade a connection to TLS
|
|
|
|
* @param socket - Client socket
|
|
|
|
*/
|
|
|
|
public startTLS(socket: plugins.net.Socket): void {
|
|
|
|
// Get the session for this socket
|
|
|
|
const session = this.sessionManager.getSession(socket);
|
|
|
|
|
2025-05-21 14:38:58 +00:00
|
|
|
// Use certificate strings directly without Buffer conversion
|
|
|
|
// For ASN.1 encoding issues, keep the raw format which Node.js can parse natively
|
|
|
|
const key = this.options.key.trim();
|
|
|
|
const cert = this.options.cert.trim();
|
|
|
|
const ca = this.options.ca ? this.options.ca.trim() : undefined;
|
2025-05-21 14:28:33 +00:00
|
|
|
|
|
|
|
// Log certificate buffer lengths for debugging
|
|
|
|
SmtpLogger.debug('Upgrading connection with certificates', {
|
|
|
|
keyBufferLength: key.length,
|
|
|
|
certBufferLength: cert.length,
|
|
|
|
caBufferLength: ca ? ca.length : 0
|
|
|
|
});
|
|
|
|
|
2025-05-21 14:38:58 +00:00
|
|
|
// For testing/production compatibility, allow older TLS versions
|
2025-05-21 14:28:33 +00:00
|
|
|
const context: plugins.tls.TlsOptions = {
|
|
|
|
key: key,
|
|
|
|
cert: cert,
|
|
|
|
ca: ca,
|
2025-05-21 12:52:24 +00:00
|
|
|
isServer: true,
|
2025-05-21 14:38:58 +00:00
|
|
|
// Allow older TLS versions for better compatibility with clients
|
|
|
|
minVersion: 'TLSv1',
|
|
|
|
maxVersion: 'TLSv1.3',
|
2025-05-21 14:28:33 +00:00
|
|
|
// Enforce server cipher preference for better security
|
|
|
|
honorCipherOrder: true,
|
|
|
|
// For testing, allow unauthorized (self-signed certs)
|
|
|
|
rejectUnauthorized: false,
|
2025-05-21 14:38:58 +00:00
|
|
|
// Use a more permissive cipher list for testing compatibility
|
|
|
|
ciphers: 'ALL:!aNULL',
|
2025-05-21 14:28:33 +00:00
|
|
|
// Allow legacy renegotiation for SMTP
|
|
|
|
allowRenegotiation: true,
|
|
|
|
// Handling handshake timeout
|
|
|
|
handshakeTimeout: 10000, // 10 seconds
|
2025-05-21 12:52:24 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
2025-05-21 14:38:58 +00:00
|
|
|
// Direct options approach without separate secureContext creation
|
|
|
|
// Use the simplest possible TLS setup to avoid ASN.1 errors
|
2025-05-21 14:28:33 +00:00
|
|
|
|
2025-05-21 14:38:58 +00:00
|
|
|
// Create secure socket directly with minimal options
|
2025-05-21 14:28:33 +00:00
|
|
|
const secureSocket = new plugins.tls.TLSSocket(socket, {
|
|
|
|
isServer: true,
|
2025-05-21 14:38:58 +00:00
|
|
|
key: key,
|
|
|
|
cert: cert,
|
|
|
|
ca: ca,
|
|
|
|
minVersion: 'TLSv1',
|
|
|
|
maxVersion: 'TLSv1.3',
|
|
|
|
ciphers: 'ALL',
|
|
|
|
honorCipherOrder: true,
|
2025-05-21 14:28:33 +00:00
|
|
|
requestCert: false,
|
|
|
|
rejectUnauthorized: false
|
|
|
|
});
|
|
|
|
|
|
|
|
// Add a specific check for secure event to make sure the handshake completes
|
|
|
|
let secureEventFired = false;
|
2025-05-21 12:52:24 +00:00
|
|
|
|
2025-05-21 14:28:33 +00:00
|
|
|
// Add specific timeout for 'secure' event
|
|
|
|
const secureEventTimeout = setTimeout(() => {
|
|
|
|
if (!secureEventFired) {
|
|
|
|
SmtpLogger.error('TLS handshake timed out waiting for secure event', {
|
|
|
|
remoteAddress: socket.remoteAddress,
|
|
|
|
remotePort: socket.remotePort
|
|
|
|
});
|
|
|
|
|
|
|
|
// Destroy the socket if secure event did not fire
|
|
|
|
socket.destroy();
|
|
|
|
}
|
|
|
|
}, 5000); // 5 second timeout
|
2025-05-21 12:52:24 +00:00
|
|
|
|
2025-05-21 14:28:33 +00:00
|
|
|
// Log the upgrade attempt
|
2025-05-21 12:52:24 +00:00
|
|
|
if (session) {
|
2025-05-21 14:28:33 +00:00
|
|
|
SmtpLogger.info(`Attempting to upgrade connection to TLS for session ${session.id}`, {
|
2025-05-21 12:52:24 +00:00
|
|
|
sessionId: session.id,
|
|
|
|
remoteAddress: session.remoteAddress
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Securely handle TLS errors
|
|
|
|
secureSocket.on('error', (err) => {
|
2025-05-21 14:28:33 +00:00
|
|
|
clearTimeout(secureEventTimeout);
|
|
|
|
|
|
|
|
SmtpLogger.error(`TLS error during STARTTLS: ${err.message}`, {
|
2025-05-21 12:52:24 +00:00
|
|
|
remoteAddress: socket.remoteAddress,
|
|
|
|
remotePort: socket.remotePort,
|
2025-05-21 14:28:33 +00:00
|
|
|
error: err,
|
|
|
|
stack: err.stack
|
2025-05-21 12:52:24 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Log security event
|
|
|
|
SmtpLogger.logSecurityEvent(
|
|
|
|
SecurityLogLevel.ERROR,
|
|
|
|
SecurityEventType.TLS_NEGOTIATION,
|
2025-05-21 14:28:33 +00:00
|
|
|
'TLS error during STARTTLS',
|
|
|
|
{ error: err.message, stack: err.stack },
|
2025-05-21 12:52:24 +00:00
|
|
|
socket.remoteAddress
|
|
|
|
);
|
|
|
|
|
|
|
|
socket.destroy();
|
|
|
|
});
|
|
|
|
|
2025-05-21 14:28:33 +00:00
|
|
|
// Log TLS connection details on secure event
|
2025-05-21 12:52:24 +00:00
|
|
|
secureSocket.on('secure', () => {
|
2025-05-21 14:28:33 +00:00
|
|
|
clearTimeout(secureEventTimeout);
|
|
|
|
secureEventFired = true;
|
|
|
|
|
2025-05-21 12:52:24 +00:00
|
|
|
const tlsDetails = getTlsDetails(secureSocket);
|
|
|
|
|
2025-05-21 14:28:33 +00:00
|
|
|
SmtpLogger.info('TLS connection successfully established via STARTTLS', {
|
|
|
|
remoteAddress: secureSocket.remoteAddress,
|
|
|
|
remotePort: secureSocket.remotePort,
|
|
|
|
protocol: tlsDetails?.protocol || 'unknown',
|
|
|
|
cipher: tlsDetails?.cipher || 'unknown',
|
|
|
|
authorized: tlsDetails?.authorized || false
|
|
|
|
});
|
|
|
|
|
|
|
|
// Log security event with TLS details
|
|
|
|
SmtpLogger.logSecurityEvent(
|
|
|
|
SecurityLogLevel.INFO,
|
|
|
|
SecurityEventType.TLS_NEGOTIATION,
|
|
|
|
'STARTTLS successful',
|
|
|
|
{
|
|
|
|
protocol: tlsDetails?.protocol,
|
|
|
|
cipher: tlsDetails?.cipher,
|
|
|
|
authorized: tlsDetails?.authorized
|
|
|
|
},
|
|
|
|
secureSocket.remoteAddress,
|
|
|
|
undefined,
|
|
|
|
true
|
|
|
|
);
|
|
|
|
|
|
|
|
// Update session if we have one
|
|
|
|
if (session) {
|
|
|
|
// Update session properties
|
|
|
|
session.useTLS = true;
|
|
|
|
session.secure = true;
|
2025-05-21 12:52:24 +00:00
|
|
|
|
2025-05-21 14:28:33 +00:00
|
|
|
// Reset session state (per RFC 3207)
|
|
|
|
// After STARTTLS, client must issue a new EHLO
|
|
|
|
if (this.sessionManager.updateSessionState) {
|
|
|
|
this.sessionManager.updateSessionState(session, SmtpState.GREETING);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const socketDetails = getSocketDetails(socket);
|
|
|
|
SmtpLogger.info(`Upgraded connection to TLS without session from ${socketDetails.remoteAddress}:${socketDetails.remotePort}`);
|
2025-05-21 12:52:24 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
|
|
|
|
remoteAddress: socket.remoteAddress,
|
|
|
|
remotePort: socket.remotePort,
|
2025-05-21 14:28:33 +00:00
|
|
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
|
|
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
2025-05-21 12:52:24 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Log security event
|
|
|
|
SmtpLogger.logSecurityEvent(
|
|
|
|
SecurityLogLevel.ERROR,
|
|
|
|
SecurityEventType.TLS_NEGOTIATION,
|
|
|
|
'Failed to upgrade connection to TLS',
|
2025-05-21 14:28:33 +00:00
|
|
|
{
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
|
|
|
},
|
2025-05-21 12:52:24 +00:00
|
|
|
socket.remoteAddress,
|
|
|
|
undefined,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
|
|
|
|
socket.destroy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a secure server
|
|
|
|
* @returns TLS server instance or undefined if TLS is not enabled
|
|
|
|
*/
|
|
|
|
public createSecureServer(): plugins.tls.Server | undefined {
|
|
|
|
if (!this.isTlsEnabled()) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2025-05-21 14:38:58 +00:00
|
|
|
// Use certificate strings directly without Buffer conversion
|
|
|
|
// For ASN.1 encoding issues, keep the raw format which Node.js can parse natively
|
|
|
|
const key = this.options.key.trim();
|
|
|
|
const cert = this.options.cert.trim();
|
|
|
|
const ca = this.options.ca ? this.options.ca.trim() : undefined;
|
2025-05-21 14:28:33 +00:00
|
|
|
|
|
|
|
// Log certificate buffer lengths for debugging
|
|
|
|
SmtpLogger.debug('Creating secure server with certificates', {
|
|
|
|
keyBufferLength: key.length,
|
|
|
|
certBufferLength: cert.length,
|
|
|
|
caBufferLength: ca ? ca.length : 0
|
|
|
|
});
|
|
|
|
|
2025-05-21 14:38:58 +00:00
|
|
|
// Simplify options to minimal necessary for test compatibility
|
2025-05-21 14:28:33 +00:00
|
|
|
const context: plugins.tls.TlsOptions = {
|
|
|
|
key: key,
|
|
|
|
cert: cert,
|
|
|
|
ca: ca,
|
2025-05-21 14:38:58 +00:00
|
|
|
// Allow all TLS versions for maximum compatibility
|
|
|
|
minVersion: 'TLSv1',
|
|
|
|
maxVersion: 'TLSv1.3',
|
|
|
|
// Accept all ciphers for testing
|
|
|
|
ciphers: 'ALL',
|
|
|
|
// For testing, always allow self-signed certs
|
2025-05-21 14:28:33 +00:00
|
|
|
rejectUnauthorized: false,
|
2025-05-21 14:38:58 +00:00
|
|
|
// Shorter handshake timeout for testing
|
|
|
|
handshakeTimeout: 5000
|
2025-05-21 12:52:24 +00:00
|
|
|
};
|
|
|
|
|
2025-05-21 14:28:33 +00:00
|
|
|
// Create a simple, standalone server that explicitly doesn't try to
|
|
|
|
// verify or validate client certificates for testing
|
2025-05-21 12:52:24 +00:00
|
|
|
return new plugins.tls.Server(context);
|
|
|
|
} catch (error) {
|
|
|
|
SmtpLogger.error(`Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, {
|
2025-05-21 14:28:33 +00:00
|
|
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
|
|
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
2025-05-21 12:52:24 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if TLS is enabled
|
|
|
|
* @returns Whether TLS is enabled
|
|
|
|
*/
|
|
|
|
public isTlsEnabled(): boolean {
|
|
|
|
return !!(this.options.key && this.options.cert);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send a response to the client
|
|
|
|
* @param socket - Client socket
|
|
|
|
* @param response - Response message
|
|
|
|
*/
|
|
|
|
private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
|
|
|
|
try {
|
|
|
|
socket.write(`${response}\r\n`);
|
|
|
|
SmtpLogger.logResponse(response, socket);
|
|
|
|
} catch (error) {
|
|
|
|
SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, {
|
|
|
|
response,
|
|
|
|
remoteAddress: socket.remoteAddress,
|
|
|
|
remotePort: socket.remotePort,
|
|
|
|
error: error instanceof Error ? error : new Error(String(error))
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.destroy();
|
|
|
|
}
|
|
|
|
}
|
2025-05-21 14:28:33 +00:00
|
|
|
}
|