283 lines
10 KiB
TypeScript
283 lines
10 KiB
TypeScript
/**
|
|
* Certificate Utilities for SMTP Server
|
|
* Provides utilities for managing TLS certificates
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as tls from 'tls';
|
|
import { SmtpLogger } from './utils/logging.js';
|
|
|
|
/**
|
|
* Certificate data
|
|
*/
|
|
export interface ICertificateData {
|
|
key: Buffer;
|
|
cert: Buffer;
|
|
ca?: Buffer;
|
|
}
|
|
|
|
/**
|
|
* Normalize a PEM certificate string
|
|
* @param str - Certificate string
|
|
* @returns Normalized certificate string
|
|
*/
|
|
function normalizeCertificate(str: string): string {
|
|
if (!str) {
|
|
throw new Error('Empty certificate data');
|
|
}
|
|
|
|
// Remove any whitespace around the string
|
|
let normalizedStr = str.trim();
|
|
|
|
// Make sure it has proper PEM format
|
|
if (!normalizedStr.includes('-----BEGIN ')) {
|
|
throw new Error('Invalid certificate format: Missing BEGIN marker');
|
|
}
|
|
|
|
if (!normalizedStr.includes('-----END ')) {
|
|
throw new Error('Invalid certificate format: Missing END marker');
|
|
}
|
|
|
|
// Normalize line endings (replace Windows-style \r\n with Unix-style \n)
|
|
normalizedStr = normalizedStr.replace(/\r\n/g, '\n');
|
|
|
|
// Ensure proper line breaks after header and before footer
|
|
const beginMatch = normalizedStr.match(/^(-----BEGIN [^-]+-----)(.*)$/s);
|
|
const endMatch = normalizedStr.match(/(.*)(-----END [^-]+-----)$/s);
|
|
|
|
if (beginMatch && endMatch) {
|
|
const header = beginMatch[1];
|
|
const footer = endMatch[2];
|
|
let content = normalizedStr.substring(header.length, normalizedStr.length - footer.length);
|
|
|
|
// Clean up any existing line breaks or spaces in the content
|
|
content = content.replace(/[\n\r\s]/g, '');
|
|
|
|
// Add proper line breaks (every 64 characters)
|
|
let formattedContent = '';
|
|
for (let i = 0; i < content.length; i += 64) {
|
|
formattedContent += content.substring(i, Math.min(i + 64, content.length)) + '\n';
|
|
}
|
|
|
|
// Reconstruct the certificate
|
|
return header + '\n' + formattedContent + footer;
|
|
}
|
|
|
|
return normalizedStr;
|
|
}
|
|
|
|
/**
|
|
* Load certificates from PEM format strings
|
|
* @param options - Certificate options
|
|
* @returns Certificate data with Buffer format
|
|
*/
|
|
export function loadCertificatesFromString(options: {
|
|
key: string;
|
|
cert: string;
|
|
ca?: string;
|
|
}): ICertificateData {
|
|
try {
|
|
// Try to fix and normalize certificates
|
|
try {
|
|
const key = normalizeCertificate(options.key);
|
|
const cert = normalizeCertificate(options.cert);
|
|
const ca = options.ca ? normalizeCertificate(options.ca) : undefined;
|
|
|
|
// Convert to Buffer with explicit utf8 encoding
|
|
const keyBuffer = Buffer.from(key, 'utf8');
|
|
const certBuffer = Buffer.from(cert, 'utf8');
|
|
const caBuffer = ca ? Buffer.from(ca, 'utf8') : undefined;
|
|
|
|
// Log for debugging
|
|
SmtpLogger.debug('Certificate properties', {
|
|
keyLength: keyBuffer.length,
|
|
certLength: certBuffer.length,
|
|
caLength: caBuffer ? caBuffer.length : 0
|
|
});
|
|
|
|
// Validate the certificates by attempting to create a secure context
|
|
try {
|
|
const secureContext = tls.createSecureContext({
|
|
key: keyBuffer,
|
|
cert: certBuffer,
|
|
ca: caBuffer
|
|
});
|
|
|
|
// If createSecureContext doesn't throw, the certificates are valid
|
|
SmtpLogger.info('Successfully validated certificate format');
|
|
} catch (validationError) {
|
|
SmtpLogger.error(`Certificate validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`);
|
|
throw validationError;
|
|
}
|
|
|
|
return {
|
|
key: keyBuffer,
|
|
cert: certBuffer,
|
|
ca: caBuffer
|
|
};
|
|
} catch (innerError) {
|
|
SmtpLogger.warn(`Certificate normalization failed: ${innerError instanceof Error ? innerError.message : String(innerError)}`);
|
|
throw innerError;
|
|
}
|
|
} catch (error) {
|
|
SmtpLogger.error(`Error loading certificates: ${error instanceof Error ? error.message : String(error)}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load certificates from files
|
|
* @param options - Certificate file paths
|
|
* @returns Certificate data with Buffer format
|
|
*/
|
|
export function loadCertificatesFromFiles(options: {
|
|
keyPath: string;
|
|
certPath: string;
|
|
caPath?: string;
|
|
}): ICertificateData {
|
|
try {
|
|
// Read files directly as Buffers
|
|
const key = fs.readFileSync(options.keyPath);
|
|
const cert = fs.readFileSync(options.certPath);
|
|
const ca = options.caPath ? fs.readFileSync(options.caPath) : undefined;
|
|
|
|
// Log for debugging
|
|
SmtpLogger.debug('Certificate file properties', {
|
|
keyLength: key.length,
|
|
certLength: cert.length,
|
|
caLength: ca ? ca.length : 0
|
|
});
|
|
|
|
// Validate the certificates by attempting to create a secure context
|
|
try {
|
|
const secureContext = tls.createSecureContext({
|
|
key,
|
|
cert,
|
|
ca
|
|
});
|
|
|
|
// If createSecureContext doesn't throw, the certificates are valid
|
|
SmtpLogger.info('Successfully validated certificate files');
|
|
} catch (validationError) {
|
|
SmtpLogger.error(`Certificate file validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`);
|
|
throw validationError;
|
|
}
|
|
|
|
return {
|
|
key,
|
|
cert,
|
|
ca
|
|
};
|
|
} catch (error) {
|
|
SmtpLogger.error(`Error loading certificate files: ${error instanceof Error ? error.message : String(error)}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate self-signed certificates for testing
|
|
* @returns Certificate data with Buffer format
|
|
*/
|
|
export function generateSelfSignedCertificates(): ICertificateData {
|
|
// This is for fallback/testing only - log a warning
|
|
SmtpLogger.warn('Generating self-signed certificates for testing - DO NOT USE IN PRODUCTION');
|
|
|
|
// Create selfsigned certificates using node-forge or similar library
|
|
// For now, use hardcoded certificates as a last resort
|
|
const key = Buffer.from(`-----BEGIN PRIVATE KEY-----
|
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEgJW1HdJPACGB
|
|
ifoL3PB+HdAVA2nUmMfq43JbIUPXGTxCtzmQhuV04WjITwFw1loPx3ReHh4KR5yJ
|
|
BVdzUDocHuauMmBycHAjv7mImR/VkuK/SwT0Q5G/9/M55o6HUNol0UKt+uZuBy1r
|
|
ggFTdTDLw86i9UG5CZbWF/Yb/DTRoAkCr7iLnaZhhhqcdh5BGj7JBylIAV5RIW1y
|
|
xQxJVJZQT2KgCeCnHRRvYRQ7tVzUQBcSvtW4zYtqK4C39BgRyLUZQVYB7siGT/uP
|
|
YJE7R73u0xEgDMFWR1pItUYcVQXHQJ+YsLVCzqI22Mik7URdwxoSHSXRYKn6wnKg
|
|
4JYg65JnAgMBAAECggEAM2LlwRhwP0pnLlLHiPE4jJ3Qdz/NUF0hLnRhcUwW1iJ1
|
|
03jzCQ4QZ3etfL9O2hVJg49J+QUG50FNduLq4SE7GZj1dEJ/YNnlk9PpI8GSpLuA
|
|
mGTUKofIEJjNy5gKR0c6/rfgP8UXYSbRnTnZwIXVkUYuAUJLJTBVcJlcvCwJ3/zz
|
|
C8789JyOO1CNwF3zEIALdW5X5se8V+sw5iHDrHVxkR2xgsYpBBOylFfBxbMvV5o1
|
|
i+QOD1HaXdmIvjBCnHqrjX5SDnAYwHBSB9y6WbwC+Th76QHkRNcHZH86PJVdLEUi
|
|
tBPQmQh+SjDRaZzDJvURnOFks+eEsCPVPZnQ4wgnAQKBgQD8oHwGZIZRUjnXULNc
|
|
vJoPcjLpvdHRO0kXTJHtG2au2i9jVzL9SFwH1lHQM0XdXPnR2BK4Gmgc2dRnSB9n
|
|
YPPvCgyL2RS0Y7W98yEcgBgwVOJHnPQGRNwxUfCTHgmCQ7lXjQKKG51+dBfOYP3j
|
|
w8VYbS2pqxZtzzZ5zhk2BrZJdwKBgQDHDZC+NU80f7rLEr5vpwx9epTArwXre8oj
|
|
nGgzZ9/lE14qDnITBuZPUHWc4/7U1CCmP0vVH6nFVvhN9ra9QCTJBzQ5aj0l3JM7
|
|
9j8R5QZIPqOu4+aqf0ZFEgmpBK2SAYqNrJ+YVa2T/zLF44Jlr5WiLkPTUyMxV5+k
|
|
P4ZK8QP7wQKBgQCbeLuRWCuVKNYgYjm9TA55BbJL82J+MvhcbXUccpUksJQRxMV3
|
|
98PBUW0Qw38WciJxQF4naSKD/jXYndD+wGzpKMIU+tKU+sEYMnuFnx13++K8XrAe
|
|
NQPHDsK1wRgXk5ygOHx78xnZbMmwBXNLwQXIhyO8FJpwJHj2CtYvjb+2xwKBgQCn
|
|
KW/RiAHvG6GKjCHCOTlx2qLPxUiXYCk2xwvRnNfY5+2PFoqMI/RZLT/41kTda1fA
|
|
TDw+j4Uu/fF2ChPadwRiUjXZzZx/UjcMJXTpQ2kpbGJ11U/cL4+Tk0S6wz+HoS7z
|
|
w3vXT9UoDyFxDBjuMQJxJWTjmymaYUtNnz4iMuRqwQKBgH+HKbYHCZaIzXRMEO5S
|
|
T3xDMYH59dTEKKXEOA1KJ9Zo5XSD8NE9SQ+9etoOcEq8tdYS45OkHD3VyFQa7THu
|
|
58awjTdkpSmMPsw3AElOYDYJgD9oxKtTjwkXHqMjDBQZrXqzOImOAJhEVL+XH3LP
|
|
lv6RZ47YRC88T+P6n1yg6BPp
|
|
-----END PRIVATE KEY-----`, 'utf8');
|
|
|
|
const cert = Buffer.from(`-----BEGIN CERTIFICATE-----
|
|
MIIDCTCCAfGgAwIBAgIUHxmGQOQoiSbzqh6hIe+7h9xDXIUwDQYJKoZIhvcNAQEL
|
|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDUyMTE2MDAzM1oXDTI2MDUy
|
|
MTE2MDAzM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
|
AAOCAQ8AMIIBCgKCAQEAxICVtR3STwAhgYn6C9zwfh3QFQNp1JjH6uNyWyFD1xk8
|
|
Qrc5kIbldOFoyE8BcNZaD8d0Xh4eCkeciwOV3FwHR4brjJgcnRwI7+5iJkf1ZLiv
|
|
0sE9EORv/fzOeaOh1DaJdFCrfrmbgdgOUm62WNQOB2hq0kggjh/S1K+TBfF+8QFs
|
|
XQyW7y7mHecNgCgK/pI5b1irdajRc7nLvzM/U8qNn4jjrLsRoYqBPpn7aLKIBrmN
|
|
pNSIe18q8EYWkdmWBcnsZpAYv75SJG8E0lAYpMv9OEUIwsPh7AYUdkZqKtFxVxV5
|
|
bYlA5ZfnVnWrWEwRXaVdFFRXIjP+EFkGYYWThbvAIb0TPQIDAQABo1MwUTAdBgNV
|
|
HQ4EFgQUiW1MoYR8YK9KJTyip5oFoUVJoCgwHwYDVR0jBBgwFoAUiW1MoYR8YK9K
|
|
JTyip5oFoUVJoCgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
|
|
BToM8SbUQXwJ9rTlQB2QI2GJaFwTpCFoQZwGUOCkwGLM3nOPLEbNPMDoIKGPwenB
|
|
P1xL8uJEgYRqP6UG/xy3HsxYsLCxuoxGGP2QjuiQKnFl0n85usZ5flCxmLC5IzYx
|
|
FLcR6WPTdj6b5JX0tM8Bi6toQ9Pj3u3dSVPZKRLYvJvZKt1PXI8qsHD/LvNa2wGG
|
|
Zi1BQFAr2cScNYa+p6IYDJi9TBNxoBIHNTzQPfWaen4MHRJqUNZCzQXcOnU/NW5G
|
|
+QqQSEMmk8yGucEHWUMFrEbABVgYuBslICEEtBZALB2jZJYSaJnPOJCcmFrxUv61
|
|
ORWZbz+8rBL0JIeA7eFxEA==
|
|
-----END CERTIFICATE-----`, 'utf8');
|
|
|
|
return {
|
|
key,
|
|
cert
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create TLS options for secure server or STARTTLS
|
|
* @param certificates - Certificate data
|
|
* @param isServer - Whether this is for server (true) or client (false)
|
|
* @returns TLS options
|
|
*/
|
|
export function createTlsOptions(
|
|
certificates: ICertificateData,
|
|
isServer: boolean = true
|
|
): tls.TlsOptions {
|
|
const options: tls.TlsOptions = {
|
|
key: certificates.key,
|
|
cert: certificates.cert,
|
|
ca: certificates.ca,
|
|
// Support a wider range of TLS versions for better compatibility
|
|
minVersion: 'TLSv1', // Support older TLS versions (minimum TLS 1.0)
|
|
maxVersion: 'TLSv1.3', // Support latest TLS version (1.3)
|
|
// Cipher suites for broad compatibility
|
|
ciphers: 'HIGH:MEDIUM:!aNULL:!eNULL:!NULL:!ADH:!RC4',
|
|
// For testing, allow unauthorized (self-signed certs)
|
|
rejectUnauthorized: false,
|
|
// Longer handshake timeout for reliability
|
|
handshakeTimeout: 30000,
|
|
// Allow renegotiation for better compatibility
|
|
allowRenegotiation: true,
|
|
// Increase timeout for better reliability under test conditions
|
|
sessionTimeout: 600,
|
|
// Let the client choose the cipher for better compatibility
|
|
honorCipherOrder: false,
|
|
// For debugging
|
|
enableTrace: true,
|
|
// Disable secure options to allow more flexibility
|
|
secureOptions: 0
|
|
};
|
|
|
|
// Server-specific options
|
|
if (isServer) {
|
|
options.ALPNProtocols = ['smtp']; // Accept non-ALPN connections (legacy clients)
|
|
}
|
|
|
|
return options;
|
|
} |