This commit is contained in:
parent
e2768902aa
commit
3ba9039438
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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!")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user