You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用go-msgauth/dkim库进行DKIM验证时持续报“body hash did not verify”错误的排查求助

使用go-msgauth/dkim库进行DKIM验证时持续报“body hash did not verify”错误的排查求助

大家好,我最近在使用go-msgauth/dkim库做DKIM验证时碰到了个头疼的问题:直接读取本地.eml文件转成[]bytes传入Verify方法时验证完全正常,但通过业务代码里的io.Pipe+WriteOriginalMail流程写入邮件内容后再验证,就一直报「body hash did not verify」错误。

问题场景还原

我的业务代码逻辑是这样的:先在goroutine里把邮件的原始头和体写入到io.Writer,然后把对应的io.Reader传给dkim.VerifyWithOptions做验证。核心代码片段如下:

go func() {
    defer func() {
        _ = w.Close()
        r := recover()
        if r != nil {
            mc.Error(LogPanicWritingMail, slog.Any("recovered", r), )
        }
    }()
    _ = message.WriteOriginalMail(mc.Context, mc.Logger, info, w)
    _, _ = w.Write([]byte("\r\n")) // 这里是我怀疑的关键点
}()

verifications, err := dkim.VerifyWithOptions(
    r,
    &dkim.VerifyOptions{
        MaxVerifications: 5,
        LookupTXT: j.lookupTXT,
    },
)

可复现的测试代码

我写了一个最小复现示例,能稳定复现这个问题:

package main

import (
    "bufio"
    "bytes"
    "context"
    "fmt"
    "io"
    "log"
    "strings"

    "github.com/emersion/go-msgauth/dkim"
)

type BodyInfo struct {
    headers string
    body string
}

// 模拟业务中写入原始邮件头和体的方法
func WriteOriginalMail(ctx context.Context, info BodyInfo, w io.Writer) error {
    _, _ = w.Write([]byte(info.headers))
    _, _ = w.Write([]byte("\r\n")) // 头和体的分隔符
    _, _ = w.Write([]byte(info.body))
    return nil
}

func main() {
    ctx := context.Background()

    // 模拟DKIM签名过的邮件:空body对应的bh值是47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
    rawHeaders := strings.Join([]string{
        "DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=test;",
        " bh=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=;",
        " h=from:to:subject:date;",
        " b=abc123==",
        "From: test@example.com",
        "To: you@example.com",
        "Subject: DKIM test",
        "Date: Tue, 1 Apr 2025 12:00:00 +0000",
    }, "\r\n")
    rawBody := "\r\n" // 空body,匹配上面的bh值

    info := BodyInfo{
        headers: rawHeaders,
        body: rawBody,
    }

    r, w := io.Pipe()
    go func() {
        defer w.Close()
        _ = WriteOriginalMail(ctx, info, w)
        _, _ = w.Write([]byte("\r\n")) // 问题根源:额外写入的换行!注释这行验证就通过
    }()

    verifications, err := dkim.VerifyWithOptions(
        r,
        &dkim.VerifyOptions{
            MaxVerifications: 5,
            LookupTXT: func(ctx context.Context, domain string) ([]string, error) {
                // 模拟TXT记录查询,这里不实际验证签名仅测试hash
                return []string{}, nil
            },
        },
    )

    if err != nil {
        log.Fatalf("DKIM verify failed: %v", err)
    }

    for _, v := range verifications {
        fmt.Printf("Result: err=%v, domain=%s\n", v.Err, v.Domain)
    }
}

我排查到的线索

在复现代码里,我把goroutine里那行额外的w.Write([]byte("\r\n"))注释掉之后,DKIM验证立刻就通过了!这说明额外写入的换行符修改了原始邮件的body内容,导致计算出的body hash和DKIM签名里的bh字段不匹配。

另外我也看了业务中实际的WriteOriginalMail实现,它本身已经处理了头和体的分隔(写入了\r\n),并且通过io.Copy把原始body完整写入:

func WriteOriginalMail(
    ctx context.Context, logger *slog.Logger, info BodyInfo, w io.Writer,
) error {
    headers := info.ReadHeaders(logger)
    body := info.ReadBody(logger)
    _ = headers.Write(w)
    _, _ = w.Write([]byte("\r\n")) // 头和体的分隔
    bodyR := body.ReadPart(logger).OriginalReader(ctx)
    _, err := io.Copy(w, bodyR)
    return err
}

想请教的问题

  1. 是不是这个额外的\r\n确实是导致body hash验证失败的根源?
  2. 在通过io.Pipe传递邮件内容给DKIM验证时,有没有更稳妥的方式确保写入的内容和原始签名的邮件完全一致,避免这类意外修改?

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 12:12:59