398 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			398 lines
		
	
	
		
			14 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.ts';
 | |
| 
 | |
| /**
 | |
|  * 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 | Buffer): string {
 | |
|   // Handle different input types
 | |
|   let inputStr: string;
 | |
|   
 | |
|   if (Buffer.isBuffer(str)) {
 | |
|     // Convert Buffer to string using utf8 encoding
 | |
|     inputStr = str.toString('utf8');
 | |
|   } else if (typeof str === 'string') {
 | |
|     inputStr = str;
 | |
|   } else {
 | |
|     throw new Error('Certificate must be a string or Buffer');
 | |
|   }
 | |
|   
 | |
|   if (!inputStr) {
 | |
|     throw new Error('Empty certificate data');
 | |
|   }
 | |
| 
 | |
|   // Remove any whitespace around the string
 | |
|   let normalizedStr = inputStr.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');
 | |
|   
 | |
|   // Only normalize if the certificate appears to have formatting issues
 | |
|   // Check if the certificate is already properly formatted
 | |
|   const lines = normalizedStr.split('\n');
 | |
|   let needsReformatting = false;
 | |
|   
 | |
|   // Check for common formatting issues:
 | |
|   // 1. Missing line breaks after header/before footer
 | |
|   // 2. Lines that are too long or too short (except header/footer)
 | |
|   // 3. Multiple consecutive blank lines
 | |
|   for (let i = 0; i < lines.length; i++) {
 | |
|     const line = lines[i].trim();
 | |
|     if (line.startsWith('-----BEGIN ') || line.startsWith('-----END ')) {
 | |
|       continue; // Skip header/footer lines
 | |
|     }
 | |
|     if (line.length === 0) {
 | |
|       continue; // Skip empty lines
 | |
|     }
 | |
|     // Check if content lines are reasonable length (base64 is typically 64 chars per line)
 | |
|     if (line.length > 76) { // Allow some flexibility beyond standard 64
 | |
|       needsReformatting = true;
 | |
|       break;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   // Only reformat if necessary
 | |
|   if (needsReformatting) {
 | |
|     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 only line breaks and carriage returns, preserve base64 content
 | |
|       content = content.replace(/[\n\r]/g, '').trim();
 | |
|       
 | |
|       // 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 | Buffer;
 | |
|   cert: string | Buffer;
 | |
|   ca?: string | Buffer;
 | |
| }): ICertificateData {
 | |
|   try {
 | |
|     // First try to use certificates without normalization
 | |
|     try {
 | |
|       let keyStr: string;
 | |
|       let certStr: string;
 | |
|       let caStr: string | undefined;
 | |
|       
 | |
|       // Convert inputs to strings without aggressive normalization
 | |
|       if (Buffer.isBuffer(options.key)) {
 | |
|         keyStr = options.key.toString('utf8');
 | |
|       } else {
 | |
|         keyStr = options.key;
 | |
|       }
 | |
|       
 | |
|       if (Buffer.isBuffer(options.cert)) {
 | |
|         certStr = options.cert.toString('utf8');
 | |
|       } else {
 | |
|         certStr = options.cert;
 | |
|       }
 | |
|       
 | |
|       if (options.ca) {
 | |
|         if (Buffer.isBuffer(options.ca)) {
 | |
|           caStr = options.ca.toString('utf8');
 | |
|         } else {
 | |
|           caStr = options.ca;
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       // Simple cleanup - only normalize line endings
 | |
|       keyStr = keyStr.trim().replace(/\r\n/g, '\n');
 | |
|       certStr = certStr.trim().replace(/\r\n/g, '\n');
 | |
|       if (caStr) {
 | |
|         caStr = caStr.trim().replace(/\r\n/g, '\n');
 | |
|       }
 | |
|       
 | |
|       // Convert to buffers
 | |
|       const keyBuffer = Buffer.from(keyStr, 'utf8');
 | |
|       const certBuffer = Buffer.from(certStr, 'utf8');
 | |
|       const caBuffer = caStr ? Buffer.from(caStr, 'utf8') : undefined;
 | |
|       
 | |
|       // Test the certificates first
 | |
|       const secureContext = tls.createSecureContext({
 | |
|         key: keyBuffer,
 | |
|         cert: certBuffer,
 | |
|         ca: caBuffer
 | |
|       });
 | |
|       
 | |
|       SmtpLogger.info('Successfully validated certificates without normalization');
 | |
|       
 | |
|       return {
 | |
|         key: keyBuffer,
 | |
|         cert: certBuffer,
 | |
|         ca: caBuffer
 | |
|       };
 | |
|       
 | |
|     } catch (simpleError) {
 | |
|       SmtpLogger.warn(`Simple certificate loading failed, trying normalization: ${simpleError instanceof Error ? simpleError.message : String(simpleError)}`);
 | |
|       
 | |
|       // DEBUG: Log certificate details when simple loading fails
 | |
|       SmtpLogger.warn('Certificate loading failure details', {
 | |
|         keyType: typeof options.key,
 | |
|         certType: typeof options.cert,
 | |
|         keyIsBuffer: Buffer.isBuffer(options.key),
 | |
|         certIsBuffer: Buffer.isBuffer(options.cert),
 | |
|         keyLength: options.key ? options.key.length : 0,
 | |
|         certLength: options.cert ? options.cert.length : 0,
 | |
|         keyPreview: options.key ? (typeof options.key === 'string' ? options.key.substring(0, 50) : options.key.toString('utf8').substring(0, 50)) : 'null',
 | |
|         certPreview: options.cert ? (typeof options.cert === 'string' ? options.cert.substring(0, 50) : options.cert.toString('utf8').substring(0, 50)) : 'null'
 | |
|       });
 | |
|     }
 | |
|     
 | |
|     // Fallback: Try to fix and normalize certificates
 | |
|     try {
 | |
|       // Normalize certificates (handles both string and Buffer inputs)
 | |
|       const key = normalizeCertificate(options.key);
 | |
|       const cert = normalizeCertificate(options.cert);
 | |
|       const ca = options.ca ? normalizeCertificate(options.ca) : undefined;
 | |
|       
 | |
|       // Convert normalized strings 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) {
 | |
|         // Log detailed error information for debugging
 | |
|         SmtpLogger.error(`Certificate validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`);
 | |
|         SmtpLogger.debug('Certificate validation details', {
 | |
|           keyPreview: keyBuffer.toString('utf8').substring(0, 100) + '...',
 | |
|           certPreview: certBuffer.toString('utf8').substring(0, 100) + '...',
 | |
|           keyLength: keyBuffer.length,
 | |
|           certLength: certBuffer.length
 | |
|         });
 | |
|         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,
 | |
|     // TLS renegotiation option (removed - not supported in newer Node.ts)
 | |
|     // 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;
 | |
| } |