Skip to content

fix: avoid early DKIM verification failure with multiple signatures#294

Open
Olexandr88 wants to merge 1 commit intozkemail:mainfrom
Olexandr88:main
Open

fix: avoid early DKIM verification failure with multiple signatures#294
Olexandr88 wants to merge 1 commit intozkemail:mainfrom
Olexandr88:main

Conversation

@Olexandr88
Copy link

@Olexandr88 Olexandr88 commented Dec 24, 2025

Avoid early DKIM verification failure when multiple signatures are present.

Summary by CodeRabbit

  • Bug Fixes
    • DKIM verification now tries multiple candidate signatures per domain instead of failing on the first match.
    • Returns an empty result when no cryptographic keys are found instead of throwing an unexpected error.
    • Provides clearer error messages when no signature matches or when all verification attempts fail.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 24, 2025

📝 Walkthrough

Walkthrough

The DKIM verification logic was changed to try multiple candidate signatures per signing domain: DNS/archive key absence now returns an empty array; candidates are filtered by domain and verified in sequence, returning on first success and emitting specific errors if none succeed.

Changes

Cohort / File(s) Change Summary
DKIM Multi-Candidate Verification Logic
packages/helpers/src/dkim/index.ts
Reworked tryVerifyDKIM to return [] when no keys found, collect multiple DKIM candidates filtered by signing domain, iterate through candidates attempting verifyDKIMSignature, capture per-candidate errors, return on first successful verification, and throw a descriptive error if all candidates fail.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • saleel
  • rutefig

Poem

🐰 I hopped through keys both near and far,

Trying each signature, one by one, a star.
If none will pass, I'll tell you why —
Till one smiles true, I won't say bye. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is minimal and only provides a brief summary. It lacks the required template sections including Type of Change, Checklist items, issue reference, and detailed context about the changes. Complete the description template by adding Type of Change selection, Checklist items, issue reference (Fixes #...), and detailed motivation for why multiple DKIM signature handling is necessary.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: avoiding early DKIM verification failure when multiple signatures exist, which aligns with the changeset's logic to try multiple DKIM candidates instead of failing on the first one.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f01979 and d1c4eaf.

📒 Files selected for processing (1)
  • packages/helpers/src/dkim/index.ts
🧰 Additional context used
🪛 Biome (2.1.2)
packages/helpers/src/dkim/index.ts

[error] 173-173: expected } but instead the file ends

the file ends here

(parse)

🔇 Additional comments (1)
packages/helpers/src/dkim/index.ts (1)

126-128: Behavioral change is accurate: returning empty array instead of throwing, with proper downstream error handling.

When the resolver returns an empty array, getPublicKey() throws an ENODATA error which is caught by DkimVerifier and converted to a neutral verification result. This allows the verification loop to continue processing other signatures rather than failing immediately, enabling the PR's goal of handling multiple signatures without early failures. The final result still fails appropriately if no valid signatures are found for the target domain.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
packages/helpers/src/dkim/index.ts (2)

161-169: Critical: Loop returns the first candidate without verifying it passes.

The try-catch block only wraps a simple assignment (candidate.headers = dkimVerifier.headers) and a return statement, neither of which will throw an error. This means the loop will always return the first candidate without checking if its verification actually passed, defeating the entire purpose of this PR.

The actual verification check happens in the outer verifyDKIMSignature function at line 78, which is too late to try alternative candidates.

🔎 Proposed fix to verify candidate status before returning
  let lastError: Error | undefined;
  
  for (const candidate of candidateResults) {
-    try {
-      candidate.headers = dkimVerifier.headers;
-      return candidate;
-    } catch (e) {
-      lastError = e as Error;
-      continue;
+    candidate.headers = dkimVerifier.headers;
+    
+    // Check if this candidate passed verification
+    if (candidate.status?.result === 'pass') {
+      return candidate;
     }
+    
+    // Capture the failure reason and try the next candidate
+    lastError = new Error(
+      `DKIM verification failed: ${candidate.status?.comment || 'unknown error'}`
+    );
   }

This ensures that only a successfully verified candidate is returned, and verification failures are captured to provide meaningful error messages.


171-173: Critical: Missing closing brace for tryVerifyDKIM function.

The file ends unexpectedly at line 173 without closing the tryVerifyDKIM function that begins at line 99. This syntax error will prevent the module from loading.

🔎 Proposed fix
  throw lastError ?? new Error(
    `All DKIM signatures failed for domain ${domainToVerifyDKIM}`
  );
+}
🧹 Nitpick comments (1)
packages/helpers/src/dkim/index.ts (1)

149-157: LGTM! Clear error handling for missing DKIM signatures.

The filtering logic correctly identifies candidate results for the specified domain and provides a clear error message when no matching signatures are found.

Optional: Consider improving type safety

If possible, replace the any type with a proper type definition for the DKIM result objects to improve type safety:

-  const candidateResults = dkimVerifier.results.filter(
-    (d: any) => d.signingDomain === domainToVerifyDKIM
-  );
+  const candidateResults = dkimVerifier.results.filter(
+    (d) => d.signingDomain === domainToVerifyDKIM
+  );

This assumes dkimVerifier.results has proper typing. If not, consider adding type definitions for the DkimVerifier class.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1c4eaf and 9a86024.

📒 Files selected for processing (1)
  • packages/helpers/src/dkim/index.ts
🧰 Additional context used
🪛 Biome (2.1.2)
packages/helpers/src/dkim/index.ts

[error] 173-173: expected } but instead the file ends

the file ends here

(parse)

🔇 Additional comments (1)
packages/helpers/src/dkim/index.ts (1)

126-128: The behavior change here is intentional and properly handled. Returning an empty array when no DNS/archive keys are found allows the DkimVerifier to continue processing and report a neutral verification status instead of throwing immediately. This enables the parent function to provide a complete verification result with the specific reason for failure.

The error handling is already in place: when getPublicKey receives an empty array, it throws an ENODATA error that the DkimVerifier catches and converts to a neutral status with the comment 'no key'. The parent verifyDKIMSignature function then checks if the result is not 'pass' and throws an error that includes this comment, ensuring users see the specific reason for failure ("no key") rather than a generic DNS resolution error.

No action needed—the design correctly supports the PR objective of handling multiple signatures while propagating appropriate error messages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant