如何手动验证DKIM的bh字段?转发邮件哈希匹配问题求助
bh Field Validation Failure? Correct Method & Relaxed Canonicalization Deep Dive Hey, looks like you're hitting a common snag when verifying the DKIM bh field for a forwarded email—hash mismatch issues almost always boil down to misapplying the relaxed canonicalization rules, or using the wrong version of the email body (forwarded vs. original signed body). Let's break this down step by step.
First: The Correct bh Validation Workflow
The bh field is the Base64-encoded SHA256 hash of the email body after applying the canonicalization algorithm specified in the DKIM-Signature header (yours uses relaxed/relaxed). Here's the exact process to verify it:
- Confirm the canonicalization method from the
cparameter inDKIM-Signature(e.g.,relaxed/relaxedmeans both headers and body use relaxed mode). - Critical Note: Use the original email body that was present when the DKIM signature was generated—not the forwarded version! Forwarded emails often add extra content (like "Forwarded Message" banners or original headers) which will break the hash match.
- Apply the specified canonicalization to the body.
- Compute the SHA256 hash of the canonicalized body, then Base64-encode it. Compare this value to the
bhfield in the DKIM-Signature.
What is Relaxed Canonicalization? (RFC 6376)
Relaxed mode is designed to tolerate minor formatting differences while maintaining integrity. Let's break down the rules for headers and body:
1. Relaxed Header Canonicalization
- Convert the header field name to all lowercase (e.g.,
Message-Id→message-id). - For the header value:
- Replace any sequence of whitespace (spaces, tabs, line folds from RFC 5322) with a single space.
- Trim leading and trailing whitespace from the value.
- Concatenate the lowercase name, a colon, and the normalized value (no spaces around the colon).
2. Relaxed Body Canonicalization
- For each line in the body:
- Replace any sequence of whitespace (spaces, tabs, form feeds) within the line with a single space.
- Remove all trailing whitespace from the line (before the
\r\nline ending).
- Ensure the entire body ends with a
\r\n. - Remove all trailing empty lines (consecutive
\r\nsequences) from the end of the body—only leave one\r\nif the body is empty, or the last line with content followed by\r\n.
Fixing Your Canonicalization Code
Your Java code has a few inconsistencies with RFC 6376 that would cause hash mismatches. Here's the corrected version with explanations:
package canonicalization; import java.security.MessageDigest; import java.util.Base64; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Canonicalization { // Relaxed header canonicalization (RFC 6376 3.4.1) public String canonicalizeHeader(String name, String value) { // Lowercase header name, trim any accidental whitespace in the name String normalizedName = name.trim().toLowerCase(); // Replace all whitespace sequences (including line folds) with single space, trim edges String normalizedValue = value.replaceAll("\\s+", " ").trim(); return normalizedName + ":" + normalizedValue; } // Relaxed body canonicalization (RFC 6376 3.4.2) public String canonicalizeBody(String body) { if (body == null || body.isEmpty()) { return "\r\n"; } // Process each line: normalize inline whitespace, remove trailing whitespace Pattern linePattern = Pattern.compile("^(.*?)\\s*$", Pattern.MULTILINE); Matcher matcher = linePattern.matcher(body); StringBuilder normalizedBody = new StringBuilder(); while (matcher.find()) { // Replace inline whitespace sequences with single space String cleanedLine = matcher.group(1).replaceAll("\\s+", " "); normalizedBody.append(cleanedLine).append("\r\n"); } String result = normalizedBody.toString(); // Remove trailing empty lines (consecutive \r\n) while (result.endsWith("\r\n\r\n")) { result = result.substring(0, result.length() - 2); } // Ensure the final body ends with exactly one \r\n if (!result.endsWith("\r\n")) { result += "\r\n"; } return result; } public static void main(String[] args) { Canonicalization obj = new Canonicalization(); // Test header canonicalization System.out.println("--- Header Canonicalization ---"); System.out.println(obj.canonicalizeHeader("Date", " Wed, 24 Jan 2018 18:33:08 +0530")); System.out.println(obj.canonicalizeHeader("From", " sender <sender@headsup.co.in>")); System.out.println(obj.canonicalizeHeader("To", " \"receiver\" <receiver@gmail.com>")); // Test body canonicalization and bh calculation System.out.println("\n--- Body Canonicalization & BH Calculation ---"); String originalBody = "for forwarded mail"; String canonicalizedBody = obj.canonicalizeBody(originalBody); System.out.println("Canonicalized Body: " + canonicalizedBody); try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hashBytes = md.digest(canonicalizedBody.getBytes("UTF-8")); String computedBh = Base64.getEncoder().encodeToString(hashBytes); System.out.println("Computed BH Value: " + computedBh); } catch (Exception e) { e.printStackTrace(); } } }
Key Fixes in the Code:
- Body Line Processing: Uses regex to properly remove trailing whitespace from each line, instead of just targeting spaces before
\r\n. - Whitespace Normalization: Correctly handles all whitespace types (not just spaces/tabs) in body lines.
- Trailing Empty Lines: Properly trims consecutive empty lines from the end of the body.
- Added BH Calculation: Includes code to compute the Base64-encoded SHA256 hash directly, so you can compare it to the
bhfield in your DKIM signature.
Final Troubleshooting Tip
If you're still getting a hash mismatch after fixing the code, the most likely issue is that you're using the forwarded email body instead of the original body that was signed. Forwarded emails often modify the body (adding banners, quoting the original email) which changes the hash entirely. You'll need to retrieve the original, unmodified body from the signed email to validate the bh field correctly.
内容的提问来源于stack exchange,提问作者Shashank

