fix: try all dkim keys until signature verifies

This commit is contained in:
Yogesh Shahi
2025-10-22 20:01:13 +05:30
parent 82bf17b69b
commit d7f6a7cb94
2 changed files with 82 additions and 33 deletions

View File

@@ -249,44 +249,66 @@ export class DkimVerifier extends MessageParser {
status.comment = `body hash did not verify`;
} else {
try {
let res = await getPublicKey(
let publicKeys = await getPublicKey(
signatureHeader.type,
`${signatureHeader.selector}._domainkey.${signatureHeader.signingDomain}`,
this.minBitLength,
this.resolver,
);
publicKey = res?.publicKey;
rr = res?.rr;
modulusLength = res?.modulusLength;
// Ensure publicKeys is always an array
if (!Array.isArray(publicKeys)) {
publicKeys = [publicKeys];
}
try {
let ver_result = false;
if (!IS_BROWSER) {
ver_result = crypto.verify(
signatureHeader.signAlgo === 'rsa' ? signatureHeader.algorithm : null,
canonicalizedHeader,
publicKey,
Buffer.from(signatureHeader.parsed?.b?.value, 'base64'),
);
} else {
let ver = crypto.createVerify('RSA-SHA256');
ver.update(canonicalizedHeader);
ver_result = ver.verify(
{ key: publicKey.toString(), format: 'pem' },
Buffer.from(signatureHeader.parsed?.b?.value, 'base64'),
);
// Try each key until one verifies successfully
let verified = false;
for (let i = 0; i < publicKeys.length; i++) {
const keyData = publicKeys[i];
try {
let ver_result = false;
if (!IS_BROWSER) {
ver_result = crypto.verify(
signatureHeader.signAlgo === 'rsa' ? signatureHeader.algorithm : null,
canonicalizedHeader,
keyData.publicKey,
Buffer.from(signatureHeader.parsed?.b?.value, 'base64'),
);
} else {
let ver = crypto.createVerify('RSA-SHA256');
ver.update(canonicalizedHeader);
ver_result = ver.verify(
{ key: keyData.publicKey.toString(), format: 'pem' },
Buffer.from(signatureHeader.parsed?.b?.value, 'base64'),
);
}
if (ver_result) {
// Success! Use this key
publicKey = keyData.publicKey;
rr = keyData.rr;
modulusLength = keyData.modulusLength;
status.signedHeaders = canonicalizedHeader;
status.result = 'pass';
verified = true;
break;
}
} catch (err: any) {
// Continue to next key
}
}
status.signedHeaders = canonicalizedHeader;
status.result = ver_result ? 'pass' : 'fail';
if (status?.result === 'fail') {
status.comment = 'bad signature';
if (!verified) {
// All keys failed
status.result = 'fail';
status.comment = 'bad signature';
// Use first key for metadata
if (publicKeys[0]) {
publicKey = publicKeys[0].publicKey;
rr = publicKeys[0].rr;
modulusLength = publicKeys[0].modulusLength;
}
} catch (err: any) {
status.comment = err.message;
status.result = 'neutral';
}
} catch (err: any) {
if (err.rr) {

View File

@@ -266,14 +266,41 @@ export const getPublicKey = async (
minBitLength = minBitLength || 1024;
let list = await resolver(name, 'TXT');
let rr =
list &&
[]
.concat(list[0] || [])
// Try all available DKIM keys (for key rotation support)
if (!list || !Array.isArray(list) || list.length === 0) {
throw new CustomError('No DNS records found', 'ENODATA');
}
let validKeys = [];
let lastError;
for (let i = 0; i < list.length; i++) {
let rr = []
.concat(list[i] || [])
.join('')
.replaceAll(/\s+/g, '')
.replaceAll('"', '');
try {
const result = await processPublicKey(type, rr, minBitLength);
validKeys.push(result);
} catch (err) {
lastError = err;
// Continue to try next key
}
}
// If no valid keys found, throw the last error
if (validKeys.length === 0) {
throw lastError;
}
return validKeys;
};
const processPublicKey = async (type: string, rr: string, minBitLength: number) => {
if (rr) {
// prefix value for parsing as there is no default value
let entry = parseDkimHeaders('DNS: TXT;' + rr);
@@ -347,7 +374,7 @@ export const getPublicKey = async (
modulusLength = pubKeyData.n.bitLength();
}
if (keyType === 'rsa' && modulusLength < 1024) {
if (keyType === 'rsa' && modulusLength < minBitLength) {
throw new CustomError('RSA key too short', 'ESHORTKEY', rr);
}