修复本域有效签名邮件因SPF失败被误删
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-01 22:23:18 +08:00
parent e2768902aa
commit 3ba9039438
3 changed files with 78 additions and 14 deletions

View File

@ -120,23 +120,33 @@ func (p *Dkim) Sign(msgData string) ([]byte, error) {
}
func Check(ctx *context.Context, mail io.Reader) bool {
return CheckForDomain(ctx, mail, "")
}
verifications, err := dkim.Verify(mail)
// CheckForDomain 在指定域名时要求有效签名与发件域严格一致,避免其他域签名冒用本域。
func CheckForDomain(ctx *context.Context, mail io.Reader, domain string) bool {
return checkForDomain(ctx, mail, domain, nil)
}
func checkForDomain(ctx *context.Context, mail io.Reader, domain string, options *dkim.VerifyOptions) bool {
verifications, err := dkim.VerifyWithOptions(mail, options)
if err != nil {
log.WithContext(ctx).Warnf("DKIM Error:%v", err)
}
if len(verifications) == 0 {
return false
}
domain = strings.TrimSpace(domain)
for _, v := range verifications {
if v == nil {
continue
}
if v.Err == nil {
log.Println("Valid signature for:", v.Domain)
if domain == "" || strings.EqualFold(v.Domain, domain) {
return true
}
} else {
log.Println("Invalid signature for:", v.Domain, v.Err)
return false
}
}
return true
return false
}

View File

@ -1,19 +1,70 @@
package parsemail
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"os"
"path/filepath"
"strings"
"testing"
"git.pcux.cn/xuans/Smail/config"
"github.com/emersion/go-msgauth/dkim"
)
func TestCheckForDomainRequiresValidAlignedSignature(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
publicKey, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
t.Fatal(err)
}
options := &dkim.VerifyOptions{LookupTXT: func(domain string) ([]string, error) {
return []string{"v=DKIM1; k=rsa; p=" + base64.StdEncoding.EncodeToString(publicKey)}, nil
}}
message := "From: sender@example.com\r\nTo: recipient@example.com\r\nSubject: authentication\r\n\r\noriginal body\r\n"
for _, tc := range []struct {
name, signer, expected string
tampered bool
want bool
}{
{name: "aligned", signer: "example.com", expected: "example.com", want: true},
{name: "case_insensitive", signer: "example.com", expected: "EXAMPLE.COM", want: true},
{name: "foreign_domain", signer: "other.example", expected: "example.com"},
{name: "subdomain", signer: "sub.example.com", expected: "example.com"},
{name: "suffix_attack", signer: "example.com.other.example", expected: "example.com"},
{name: "invalid_signature", signer: "example.com", expected: "example.com", tampered: true},
{name: "generic_verification", signer: "other.example", want: true},
} {
t.Run(tc.name, func(t *testing.T) {
var signed bytes.Buffer
if err := dkim.Sign(&signed, strings.NewReader(message), &dkim.SignOptions{
Domain: tc.signer, Selector: "default", Signer: key,
}); err != nil {
t.Fatal(err)
}
data := signed.String()
if tc.tampered {
data += "tampered body\r\n"
}
if got := checkForDomain(nil, strings.NewReader(data), tc.expected, options); got != tc.want {
t.Fatalf("签名认证结果为 %v,期望 %v", got, tc.want)
}
})
}
if checkForDomain(nil, strings.NewReader(message), "example.com", options) {
t.Fatal("未签名邮件不能通过认证")
}
}
func TestLoadPrivateKeyAcceptsStrongRSAKey(t *testing.T) {
path := writeDKIMTestKey(t, 2048)
signer, err := loadPrivateKey(path)

View File

@ -192,17 +192,20 @@ func (s *Session) Data(r io.Reader) (resultErr error) {
var dkimStatus, SPFStatus bool
details.Stage = "验证收信身份与插件处理"
_, fromDomain := email.From.GetDomainAccount()
domains := config.CurrentMailDomains()
localSender := array.InArray(fromDomain, domains.Accepted)
dkimDomain := ""
if localSender {
dkimDomain = fromDomain
}
// DKIM校验
dkimStatus = parsemail.Check(ctx, bytes.NewReader(emailData))
dkimStatus = parsemail.CheckForDomain(ctx, bytes.NewReader(emailData), dkimDomain)
SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress)
_, formDomain := email.From.GetDomainAccount()
domains := config.CurrentMailDomains()
spoofed := array.InArray(formDomain, domains.Accepted) && SPFStatus == false
if spoofed {
dkimStatus = false
}
// 本机投递也保留真实 SPF 结果,仅有效且同域的 DKIM 签名可以避免误判伪造。
spoofed := localSender && !SPFStatus && !dkimStatus
email.Authentication = parsemail.NewEmailAuthentication(SPFStatus, dkimStatus)
log.WithContext(ctx).Debugf("开始执行插件ReceiveParseAfter!")