update
This commit is contained in:
@ -8,6 +8,12 @@ import type { ITlsHandler, ISessionManager } from './interfaces.js';
|
||||
import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js';
|
||||
import { SmtpLogger } from './utils/logging.js';
|
||||
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
|
||||
import {
|
||||
loadCertificatesFromString,
|
||||
generateSelfSignedCertificates,
|
||||
createTlsOptions,
|
||||
type ICertificateData
|
||||
} from './certificate-utils.js';
|
||||
import { SmtpState } from '../interfaces.js';
|
||||
|
||||
/**
|
||||
@ -29,6 +35,11 @@ export class TlsHandler implements ITlsHandler {
|
||||
rejectUnauthorized?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Certificate data
|
||||
*/
|
||||
private certificates: ICertificateData;
|
||||
|
||||
/**
|
||||
* Creates a new TLS handler
|
||||
* @param sessionManager - Session manager instance
|
||||
@ -45,6 +56,23 @@ export class TlsHandler implements ITlsHandler {
|
||||
) {
|
||||
this.sessionManager = sessionManager;
|
||||
this.options = options;
|
||||
|
||||
// Initialize certificates
|
||||
try {
|
||||
// Try to load certificates from provided options
|
||||
this.certificates = loadCertificatesFromString({
|
||||
key: options.key,
|
||||
cert: options.cert,
|
||||
ca: options.ca
|
||||
});
|
||||
|
||||
SmtpLogger.info('Successfully loaded TLS certificates');
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Failed to load certificates from options, using self-signed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
|
||||
// Fall back to self-signed certificates for testing
|
||||
this.certificates = generateSelfSignedCertificates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,139 +127,78 @@ export class TlsHandler implements ITlsHandler {
|
||||
* Upgrade a connection to TLS
|
||||
* @param socket - Client socket
|
||||
*/
|
||||
public startTLS(socket: plugins.net.Socket): void {
|
||||
public async startTLS(socket: plugins.net.Socket): Promise<void> {
|
||||
// Get the session for this socket
|
||||
const session = this.sessionManager.getSession(socket);
|
||||
|
||||
try {
|
||||
// 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;
|
||||
// Import the enhanced STARTTLS handler
|
||||
// This uses a more robust approach to TLS upgrades
|
||||
const { performStartTLS } = await import('./starttls-handler.js');
|
||||
|
||||
// Log certificate lengths for debugging
|
||||
SmtpLogger.debug('Upgrading connection with certificates', {
|
||||
keyLength: key.length,
|
||||
certLength: cert.length,
|
||||
caLength: ca ? ca.length : 0
|
||||
});
|
||||
SmtpLogger.info('Using enhanced STARTTLS implementation');
|
||||
|
||||
// Create secure socket directly with minimal options for maximum compatibility
|
||||
const secureSocket = new plugins.tls.TLSSocket(socket, {
|
||||
isServer: true,
|
||||
key: key,
|
||||
cert: cert,
|
||||
ca: ca,
|
||||
// Allow older TLS versions for better compatibility with clients
|
||||
minVersion: 'TLSv1',
|
||||
maxVersion: 'TLSv1.3',
|
||||
// Use a permissive cipher list for testing compatibility
|
||||
ciphers: 'ALL',
|
||||
// For testing, allow unauthorized (self-signed certs)
|
||||
rejectUnauthorized: false,
|
||||
// Don't request client certificates
|
||||
requestCert: false,
|
||||
// Allow legacy renegotiation for SMTP
|
||||
allowRenegotiation: true,
|
||||
// No server - prevents potential reference issues
|
||||
server: undefined
|
||||
});
|
||||
|
||||
// Add a specific check for secure event to make sure the handshake completes
|
||||
let secureEventFired = false;
|
||||
|
||||
// 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
|
||||
// Use the enhanced STARTTLS handler with better error handling and socket management
|
||||
const tlsSocket = await performStartTLS(socket, {
|
||||
key: this.options.key,
|
||||
cert: this.options.cert,
|
||||
ca: this.options.ca,
|
||||
session: session,
|
||||
// Callback for successful upgrade
|
||||
onSuccess: (secureSocket) => {
|
||||
SmtpLogger.info('TLS connection successfully established via enhanced STARTTLS', {
|
||||
remoteAddress: secureSocket.remoteAddress,
|
||||
remotePort: secureSocket.remotePort,
|
||||
protocol: secureSocket.getProtocol() || 'unknown',
|
||||
cipher: secureSocket.getCipher()?.name || 'unknown'
|
||||
});
|
||||
|
||||
// Destroy the socket if secure event did not fire
|
||||
socket.destroy();
|
||||
}
|
||||
}, 5000); // 5 second timeout
|
||||
// Log security event
|
||||
SmtpLogger.logSecurityEvent(
|
||||
SecurityLogLevel.INFO,
|
||||
SecurityEventType.TLS_NEGOTIATION,
|
||||
'STARTTLS successful with enhanced implementation',
|
||||
{
|
||||
protocol: secureSocket.getProtocol(),
|
||||
cipher: secureSocket.getCipher()?.name
|
||||
},
|
||||
secureSocket.remoteAddress,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
},
|
||||
// Callback for failed upgrade
|
||||
onFailure: (error) => {
|
||||
SmtpLogger.error(`Enhanced STARTTLS failed: ${error.message}`, {
|
||||
sessionId: session?.id,
|
||||
remoteAddress: socket.remoteAddress,
|
||||
error
|
||||
});
|
||||
|
||||
// Log security event
|
||||
SmtpLogger.logSecurityEvent(
|
||||
SecurityLogLevel.ERROR,
|
||||
SecurityEventType.TLS_NEGOTIATION,
|
||||
'Enhanced STARTTLS failed',
|
||||
{ error: error.message },
|
||||
socket.remoteAddress,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
},
|
||||
// Function to update session state
|
||||
updateSessionState: this.sessionManager.updateSessionState?.bind(this.sessionManager)
|
||||
});
|
||||
|
||||
// Log the upgrade attempt
|
||||
if (session) {
|
||||
SmtpLogger.info(`Attempting to upgrade connection to TLS for session ${session.id}`, {
|
||||
sessionId: session.id,
|
||||
remoteAddress: session.remoteAddress
|
||||
// If STARTTLS failed with the enhanced implementation, log the error
|
||||
if (!tlsSocket) {
|
||||
SmtpLogger.warn('Enhanced STARTTLS implementation failed to create TLS socket', {
|
||||
sessionId: session?.id,
|
||||
remoteAddress: socket.remoteAddress
|
||||
});
|
||||
}
|
||||
|
||||
// Securely handle TLS errors
|
||||
secureSocket.on('error', (err) => {
|
||||
clearTimeout(secureEventTimeout);
|
||||
|
||||
SmtpLogger.error(`TLS error during STARTTLS: ${err.message}`, {
|
||||
remoteAddress: socket.remoteAddress,
|
||||
remotePort: socket.remotePort,
|
||||
error: err,
|
||||
stack: err.stack
|
||||
});
|
||||
|
||||
// Log security event
|
||||
SmtpLogger.logSecurityEvent(
|
||||
SecurityLogLevel.ERROR,
|
||||
SecurityEventType.TLS_NEGOTIATION,
|
||||
'TLS error during STARTTLS',
|
||||
{ error: err.message, stack: err.stack },
|
||||
socket.remoteAddress
|
||||
);
|
||||
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
// Log TLS connection details on secure event
|
||||
secureSocket.on('secure', () => {
|
||||
clearTimeout(secureEventTimeout);
|
||||
secureEventFired = true;
|
||||
|
||||
const tlsDetails = getTlsDetails(secureSocket);
|
||||
|
||||
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;
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// Log STARTTLS failure
|
||||
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
remoteAddress: socket.remoteAddress,
|
||||
remotePort: socket.remotePort,
|
||||
@ -253,6 +220,7 @@ export class TlsHandler implements ITlsHandler {
|
||||
false
|
||||
);
|
||||
|
||||
// Destroy the socket on error
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
@ -267,40 +235,47 @@ export class TlsHandler implements ITlsHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
// 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;
|
||||
SmtpLogger.info('Creating secure TLS server');
|
||||
|
||||
// Log certificate lengths for debugging
|
||||
SmtpLogger.debug('Creating secure server with certificates', {
|
||||
keyLength: key.length,
|
||||
certLength: cert.length,
|
||||
caLength: ca ? ca.length : 0
|
||||
// Log certificate info
|
||||
SmtpLogger.debug('Using certificates for secure server', {
|
||||
keyLength: this.certificates.key.length,
|
||||
certLength: this.certificates.cert.length,
|
||||
caLength: this.certificates.ca ? this.certificates.ca.length : 0
|
||||
});
|
||||
|
||||
// Simplify options to minimal necessary for test compatibility
|
||||
// Use consistent options with startTLS for maximum compatibility
|
||||
const tlsOptions: plugins.tls.TlsOptions = {
|
||||
key: key,
|
||||
cert: cert,
|
||||
ca: ca,
|
||||
// Allow older TLS versions for better compatibility with clients
|
||||
minVersion: 'TLSv1',
|
||||
maxVersion: 'TLSv1.3',
|
||||
// Use a permissive cipher list for testing compatibility
|
||||
ciphers: 'ALL',
|
||||
// For testing, allow unauthorized (self-signed certs)
|
||||
rejectUnauthorized: false,
|
||||
// Shorter handshake timeout for testing
|
||||
handshakeTimeout: 5000,
|
||||
// Accept non-ALPN connections (legacy clients)
|
||||
ALPNProtocols: ['smtp']
|
||||
};
|
||||
// Create TLS options using our certificate utilities
|
||||
// This ensures proper PEM format handling and protocol negotiation
|
||||
const tlsOptions = createTlsOptions(this.certificates, true); // Use server options
|
||||
|
||||
// Create a simple, standalone server with minimal options
|
||||
return new plugins.tls.Server(tlsOptions);
|
||||
SmtpLogger.info('Creating TLS server with options', {
|
||||
minVersion: tlsOptions.minVersion,
|
||||
maxVersion: tlsOptions.maxVersion,
|
||||
handshakeTimeout: tlsOptions.handshakeTimeout
|
||||
});
|
||||
|
||||
// Create a server with wider TLS compatibility
|
||||
const server = new plugins.tls.Server(tlsOptions);
|
||||
|
||||
// Add error handling
|
||||
server.on('error', (err) => {
|
||||
SmtpLogger.error(`TLS server error: ${err.message}`, {
|
||||
error: err,
|
||||
stack: err.stack
|
||||
});
|
||||
});
|
||||
|
||||
// Log TLS details for each connection
|
||||
server.on('secureConnection', (socket) => {
|
||||
SmtpLogger.info('New secure connection established', {
|
||||
protocol: socket.getProtocol(),
|
||||
cipher: socket.getCipher()?.name,
|
||||
remoteAddress: socket.remoteAddress,
|
||||
remotePort: socket.remotePort
|
||||
});
|
||||
});
|
||||
|
||||
return server;
|
||||
} catch (error) {
|
||||
SmtpLogger.error(`Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
|
Reference in New Issue
Block a user