This commit is contained in:
2025-05-21 14:28:33 +00:00
parent 38811dbf23
commit 10ab09894b
8 changed files with 652 additions and 162 deletions

View File

@ -8,6 +8,7 @@ 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 { SmtpState } from '../interfaces.js';
/**
* Handles TLS functionality for SMTP server
@ -102,108 +103,158 @@ export class TlsHandler implements ITlsHandler {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
// Create TLS context
const context = {
key: this.options.key,
cert: this.options.cert,
ca: this.options.ca,
// Convert certificates to Buffer format for Node.js TLS
// This helps prevent ASN.1 encoding issues when Node parses the certificates
const key = Buffer.from(this.options.key.trim());
const cert = Buffer.from(this.options.cert.trim());
const ca = this.options.ca ? Buffer.from(this.options.ca.trim()) : undefined;
// Log certificate buffer lengths for debugging
SmtpLogger.debug('Upgrading connection with certificates', {
keyBufferLength: key.length,
certBufferLength: cert.length,
caBufferLength: ca ? ca.length : 0
});
// Use more secure TLS options aligned with SMTPServer implementation
const context: plugins.tls.TlsOptions = {
key: key,
cert: cert,
ca: ca,
isServer: true,
rejectUnauthorized: this.options.rejectUnauthorized || false
// More secure TLS version requirement
minVersion: 'TLSv1.2',
// Enforce server cipher preference for better security
honorCipherOrder: true,
// For testing, allow unauthorized (self-signed certs)
rejectUnauthorized: false,
// Use a more secure cipher list that's still compatible
ciphers: 'HIGH:!aNULL:!MD5:!RC4',
// Allow legacy renegotiation for SMTP
allowRenegotiation: true,
// Handling handshake timeout
handshakeTimeout: 10000, // 10 seconds
};
try {
// Upgrade the connection
const secureSocket = new plugins.tls.TLSSocket(socket, context);
// Instead of using new TLSSocket directly, use createServer approach
// which is more robust for STARTTLS upgrades
const serverContext = plugins.tls.createSecureContext(context);
// Store reference to the original socket to facilitate cleanup
(secureSocket as any).originalSocket = socket;
// Create empty server options
const options: plugins.tls.TlsOptions = {
...context,
secureContext: serverContext
};
// Log the successful upgrade
// Create secure socket
const secureSocket = new plugins.tls.TLSSocket(socket, {
...options,
isServer: true,
server: undefined,
requestCert: false,
rejectUnauthorized: false
});
// 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
});
// Destroy the socket if secure event did not fire
socket.destroy();
}
}, 5000); // 5 second timeout
// Log the upgrade attempt
if (session) {
SmtpLogger.info(`Upgraded connection to TLS for session ${session.id}`, {
SmtpLogger.info(`Attempting to upgrade connection to TLS for session ${session.id}`, {
sessionId: session.id,
remoteAddress: session.remoteAddress
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.INFO,
SecurityEventType.TLS_NEGOTIATION,
'STARTTLS negotiation successful',
{},
session.remoteAddress,
undefined,
true
);
// 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}`);
}
// Securely handle TLS errors
secureSocket.on('error', (err) => {
SmtpLogger.error(`TLS error: ${err.message}`, {
clearTimeout(secureEventTimeout);
SmtpLogger.error(`TLS error during STARTTLS: ${err.message}`, {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
error: err
error: err,
stack: err.stack
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.ERROR,
SecurityEventType.TLS_NEGOTIATION,
'TLS error after successful negotiation',
{ error: err.message },
'TLS error during STARTTLS',
{ error: err.message, stack: err.stack },
socket.remoteAddress
);
socket.destroy();
});
// Log TLS connection details on secure
// Log TLS connection details on secure event
secureSocket.on('secure', () => {
clearTimeout(secureEventTimeout);
secureEventFired = true;
const tlsDetails = getTlsDetails(secureSocket);
if (tlsDetails) {
SmtpLogger.info('TLS connection established', {
remoteAddress: secureSocket.remoteAddress,
remotePort: secureSocket.remotePort,
protocol: tlsDetails.protocol,
cipher: tlsDetails.cipher,
authorized: tlsDetails.authorized
});
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;
// Log security event with TLS details
SmtpLogger.logSecurityEvent(
SecurityLogLevel.INFO,
SecurityEventType.TLS_NEGOTIATION,
'TLS connection details',
{
protocol: tlsDetails.protocol,
cipher: tlsDetails.cipher,
authorized: tlsDetails.authorized
},
secureSocket.remoteAddress,
undefined,
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) {
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
error: error instanceof Error ? error : new Error(String(error))
error: error instanceof Error ? error : new Error(String(error)),
stack: error instanceof Error ? error.stack : 'No stack trace available'
});
// Log security event
@ -211,7 +262,10 @@ export class TlsHandler implements ITlsHandler {
SecurityLogLevel.ERROR,
SecurityEventType.TLS_NEGOTIATION,
'Failed to upgrade connection to TLS',
{ error: error instanceof Error ? error.message : String(error) },
{
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : 'No stack trace available'
},
socket.remoteAddress,
undefined,
false
@ -231,19 +285,49 @@ export class TlsHandler implements ITlsHandler {
}
try {
// Create TLS context
const context = {
key: this.options.key,
cert: this.options.cert,
ca: this.options.ca,
rejectUnauthorized: this.options.rejectUnauthorized || false
// Convert certificates to Buffer format for Node.js TLS
// This helps prevent ASN.1 encoding issues when Node parses the certificates
const key = Buffer.from(this.options.key.trim());
const cert = Buffer.from(this.options.cert.trim());
const ca = this.options.ca ? Buffer.from(this.options.ca.trim()) : undefined;
// Log certificate buffer lengths for debugging
SmtpLogger.debug('Creating secure server with certificates', {
keyBufferLength: key.length,
certBufferLength: cert.length,
caBufferLength: ca ? ca.length : 0
});
// Explicitly use more secure TLS options aligned with SMTPServer implementation
const context: plugins.tls.TlsOptions = {
key: key,
cert: cert,
ca: ca,
// More secure TLS version requirement
minVersion: 'TLSv1.2',
// Enforce server cipher preference for better security
honorCipherOrder: true,
// For testing, allow unauthorized (self-signed certs)
rejectUnauthorized: false,
// Enable session reuse for better performance
sessionTimeout: 300,
// Use a more secure cipher list that's still compatible
ciphers: 'HIGH:!aNULL:!MD5:!RC4',
// Allow legacy renegotiation for SMTP
allowRenegotiation: true,
// Handling handshake timeout
handshakeTimeout: 10000, // 10 seconds
// Accept non-ALPN connections (legacy clients)
ALPNProtocols: ['smtp'],
};
// Create secure server
// Create a simple, standalone server that explicitly doesn't try to
// verify or validate client certificates for testing
return new plugins.tls.Server(context);
} 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))
error: error instanceof Error ? error : new Error(String(error)),
stack: error instanceof Error ? error.stack : 'No stack trace available'
});
return undefined;
@ -278,7 +362,4 @@ export class TlsHandler implements ITlsHandler {
socket.destroy();
}
}
}
// Import SmtpState only for type reference, not available at runtime
import { SmtpState } from '../interfaces.js';
}