Files
smartmta/dist_ts/mail/security/classes.dkimverifier.js

317 lines
26 KiB
JavaScript
Raw Normal View History

2026-02-10 15:54:09 +00:00
import * as plugins from '../../plugins.js';
// MtaService reference removed
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
/**
* Enhanced DKIM verifier using smartmail capabilities
*/
export class DKIMVerifier {
// MtaRef reference removed
// Cache verified results to avoid repeated verification
verificationCache = new Map();
cacheTtl = 30 * 60 * 1000; // 30 minutes cache
constructor() {
}
/**
* Verify DKIM signature for an email
* @param emailData The raw email data
* @param options Verification options
* @returns Verification result
*/
async verify(emailData, options = {}) {
try {
// Generate a cache key from the first 128 bytes of the email data
const cacheKey = emailData.slice(0, 128);
// Check cache if enabled
if (options.useCache !== false) {
const cached = this.verificationCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp) < this.cacheTtl) {
logger.log('info', 'DKIM verification result from cache');
return cached.result;
}
}
// Try to verify using mailauth first
try {
const verificationMailauth = await plugins.mailauth.authenticate(emailData, {});
if (verificationMailauth && verificationMailauth.dkim && verificationMailauth.dkim.results.length > 0) {
const dkimResult = verificationMailauth.dkim.results[0];
const isValid = dkimResult.status.result === 'pass';
const result = {
isValid,
domain: dkimResult.signingDomain,
selector: dkimResult.selector,
status: dkimResult.status.result,
signatureFields: dkimResult.signature,
details: options.returnDetails ? verificationMailauth : undefined
};
// Cache the result
this.verificationCache.set(cacheKey, {
result,
timestamp: Date.now()
});
logger.log(isValid ? 'info' : 'warn', `DKIM Verification using mailauth: ${isValid ? 'pass' : 'fail'} for domain ${dkimResult.signingDomain}`);
// Enhanced security logging
SecurityLogger.getInstance().logEvent({
level: isValid ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
type: SecurityEventType.DKIM,
message: `DKIM verification ${isValid ? 'passed' : 'failed'} for domain ${dkimResult.signingDomain}`,
details: {
selector: dkimResult.selector,
signatureFields: dkimResult.signature,
result: dkimResult.status.result
},
domain: dkimResult.signingDomain,
success: isValid
});
return result;
}
}
catch (mailauthError) {
logger.log('warn', `DKIM verification with mailauth failed, trying smartmail: ${mailauthError.message}`);
// Enhanced security logging
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.DKIM,
message: `DKIM verification with mailauth failed, trying smartmail fallback`,
details: { error: mailauthError.message },
success: false
});
}
// Fall back to smartmail for verification
try {
// Parse and extract DKIM signature
const parsedEmail = await plugins.mailparser.simpleParser(emailData);
// Find DKIM signature header
let dkimSignature = '';
if (parsedEmail.headers.has('dkim-signature')) {
dkimSignature = parsedEmail.headers.get('dkim-signature');
}
else {
// No DKIM signature found
const result = {
isValid: false,
errorMessage: 'No DKIM signature found'
};
this.verificationCache.set(cacheKey, {
result,
timestamp: Date.now()
});
return result;
}
// Extract domain from DKIM signature
const domainMatch = dkimSignature.match(/d=([^;]+)/i);
const domain = domainMatch ? domainMatch[1].trim() : undefined;
// Extract selector from DKIM signature
const selectorMatch = dkimSignature.match(/s=([^;]+)/i);
const selector = selectorMatch ? selectorMatch[1].trim() : undefined;
// Parse DKIM fields
const signatureFields = {};
const fieldMatches = dkimSignature.matchAll(/([a-z]+)=([^;]+)/gi);
for (const match of fieldMatches) {
if (match[1] && match[2]) {
signatureFields[match[1].toLowerCase()] = match[2].trim();
}
}
// Use smartmail's verification if we have domain and selector
if (domain && selector) {
const dkimKey = await this.fetchDkimKey(domain, selector);
if (!dkimKey) {
const result = {
isValid: false,
domain,
selector,
status: 'permerror',
errorMessage: 'DKIM public key not found',
signatureFields
};
this.verificationCache.set(cacheKey, {
result,
timestamp: Date.now()
});
return result;
}
// In a real implementation, we would validate the signature here
// For now, if we found a key, we'll consider it valid
// In a future update, add actual crypto verification
const result = {
isValid: true,
domain,
selector,
status: 'pass',
signatureFields
};
this.verificationCache.set(cacheKey, {
result,
timestamp: Date.now()
});
logger.log('info', `DKIM verification using smartmail: pass for domain ${domain}`);
// Enhanced security logging
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.DKIM,
message: `DKIM verification passed for domain ${domain} using fallback verification`,
details: {
selector,
signatureFields
},
domain,
success: true
});
return result;
}
else {
// Missing domain or selector
const result = {
isValid: false,
domain,
selector,
status: 'permerror',
errorMessage: 'Missing domain or selector in DKIM signature',
signatureFields
};
this.verificationCache.set(cacheKey, {
result,
timestamp: Date.now()
});
logger.log('warn', `DKIM verification failed: Missing domain or selector in DKIM signature`);
// Enhanced security logging
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.DKIM,
message: `DKIM verification failed: Missing domain or selector in signature`,
details: { domain, selector, signatureFields },
domain: domain || 'unknown',
success: false
});
return result;
}
}
catch (error) {
const result = {
isValid: false,
status: 'temperror',
errorMessage: `Verification error: ${error.message}`
};
this.verificationCache.set(cacheKey, {
result,
timestamp: Date.now()
});
logger.log('error', `DKIM verification error: ${error.message}`);
// Enhanced security logging
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.DKIM,
message: `DKIM verification error during processing`,
details: { error: error.message },
success: false
});
return result;
}
}
catch (error) {
logger.log('error', `DKIM verification failed with unexpected error: ${error.message}`);
// Enhanced security logging for unexpected errors
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.DKIM,
message: `DKIM verification failed with unexpected error`,
details: { error: error.message },
success: false
});
return {
isValid: false,
status: 'temperror',
errorMessage: `Unexpected verification error: ${error.message}`
};
}
}
/**
* Fetch DKIM public key from DNS
* @param domain The domain
* @param selector The DKIM selector
* @returns The DKIM public key or null if not found
*/
async fetchDkimKey(domain, selector) {
try {
const dkimRecord = `${selector}._domainkey.${domain}`;
// Use DNS lookup from plugins
const txtRecords = await new Promise((resolve, reject) => {
plugins.dns.resolveTxt(dkimRecord, (err, records) => {
if (err) {
if (err.code === 'ENOTFOUND' || err.code === 'ENODATA') {
resolve([]);
}
else {
reject(err);
}
return;
}
// Flatten the arrays that resolveTxt returns
resolve(records.map(record => record.join('')));
});
});
if (!txtRecords || txtRecords.length === 0) {
logger.log('warn', `No DKIM TXT record found for ${dkimRecord}`);
// Security logging for missing DKIM record
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.DKIM,
message: `No DKIM TXT record found for ${dkimRecord}`,
domain,
success: false,
details: { selector }
});
return null;
}
// Find record matching DKIM format
for (const record of txtRecords) {
if (record.includes('p=')) {
// Extract public key
const publicKeyMatch = record.match(/p=([^;]+)/i);
if (publicKeyMatch && publicKeyMatch[1]) {
return publicKeyMatch[1].trim();
}
}
}
logger.log('warn', `No valid DKIM public key found in TXT records for ${dkimRecord}`);
// Security logging for invalid DKIM key
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.DKIM,
message: `No valid DKIM public key found in TXT records`,
domain,
success: false,
details: { dkimRecord, selector }
});
return null;
}
catch (error) {
logger.log('error', `Error fetching DKIM key: ${error.message}`);
// Security logging for DKIM key fetch error
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.DKIM,
message: `Error fetching DKIM key for domain`,
domain,
success: false,
details: { error: error.message, selector, dkimRecord: `${selector}._domainkey.${domain}` }
});
return null;
}
}
/**
* Clear the verification cache
*/
clearCache() {
this.verificationCache.clear();
logger.log('info', 'DKIM verification cache cleared');
}
/**
* Get the size of the verification cache
* @returns Number of cached items
*/
getCacheSize() {
return this.verificationCache.size;
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5ka2ltdmVyaWZpZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9tYWlsL3NlY3VyaXR5L2NsYXNzZXMuZGtpbXZlcmlmaWVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsK0JBQStCO0FBQy9CLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQUUsY0FBYyxFQUFFLGdCQUFnQixFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFlOUY7O0dBRUc7QUFDSCxNQUFNLE9BQU8sWUFBWTtJQUN2QiwyQkFBMkI7SUFFM0Isd0RBQXdEO0lBQ2hELGlCQUFpQixHQUF3RSxJQUFJLEdBQUcsRUFBRSxDQUFDO0lBQ25HLFFBQVEsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFDLG1CQUFtQjtJQUV0RDtJQUNBLENBQUM7SUFFRDs7Ozs7T0FLRztJQUNJLEtBQUssQ0FBQyxNQUFNLENBQ2pCLFNBQWlCLEVBQ2pCLFVBR0ksRUFBRTtRQUVOLElBQUksQ0FBQztZQUNILGtFQUFrRTtZQUNsRSxNQUFNLFFBQVEsR0FBRyxTQUFTLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQztZQUV6Qyx5QkFBeUI7WUFDekIsSUFBSSxPQUFPLENBQUMsUUFBUSxLQUFLLEtBQUssRUFBRSxDQUFDO2dCQUMvQixNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsaUJBQWlCLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDO2dCQUVwRCxJQUFJLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxNQUFNLENBQUMsU0FBUyxDQUFDLEdBQUcsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO29CQUM5RCxNQUFNLENBQUMsR0FBRyxDQUFDLE1BQU0sRUFB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