修复站内回环投递依赖公网解析导致误判并补充验签回归
This commit is contained in:
parent
04c1f236f1
commit
7b44f01d15
@ -5,6 +5,7 @@ import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
@ -14,6 +15,7 @@ import (
|
||||
"github.com/emersion/go-msgauth/dkim"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@ -123,6 +125,42 @@ func Check(ctx *context.Context, mail io.Reader) bool {
|
||||
return CheckForDomain(ctx, mail, "")
|
||||
}
|
||||
|
||||
// CheckLocalDelivery 仅对回环投递使用当前主域公钥验签,不能仅凭来源地址放行。
|
||||
func CheckLocalDelivery(ctx *context.Context, mail io.Reader, domain string, remote net.Addr) bool {
|
||||
if instance == nil {
|
||||
return false
|
||||
}
|
||||
return checkLocalDelivery(ctx, mail, domain, config.CurrentMailDomains().Primary, remote, instance.privateKey)
|
||||
}
|
||||
|
||||
func checkLocalDelivery(ctx *context.Context, mail io.Reader, domain, primary string, remote net.Addr, signer crypto.Signer) bool {
|
||||
address, ok := remote.(*net.TCPAddr)
|
||||
if !ok || address == nil || !address.IP.IsLoopback() || signer == nil {
|
||||
return false
|
||||
}
|
||||
domain = strings.TrimSpace(domain)
|
||||
primary = strings.TrimSpace(primary)
|
||||
if primary == "" || !strings.EqualFold(domain, primary) {
|
||||
return false
|
||||
}
|
||||
publicKey, err := x509.MarshalPKIXPublicKey(signer.Public())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// 只提供本机实际使用的选择器和域名公钥,其他签名交回正常 DNS 验证流程。
|
||||
options := &dkim.VerifyOptions{LookupTXT: func(name string) ([]string, error) {
|
||||
if !strings.EqualFold(name, "default._domainkey."+primary) {
|
||||
return nil, fmt.Errorf("signature does not use the current local DKIM selector")
|
||||
}
|
||||
return []string{"v=DKIM1; k=rsa; p=" + base64.StdEncoding.EncodeToString(publicKey)}, nil
|
||||
}}
|
||||
verified := checkForDomain(ctx, mail, domain, options)
|
||||
if verified {
|
||||
log.WithContext(ctx).Info("DKIM local delivery signature verified")
|
||||
}
|
||||
return verified
|
||||
}
|
||||
|
||||
// CheckForDomain 在指定域名时要求有效签名与发件域严格一致,避免其他域签名冒用本域。
|
||||
func CheckForDomain(ctx *context.Context, mail io.Reader, domain string) bool {
|
||||
return checkForDomain(ctx, mail, domain, nil)
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -65,6 +66,71 @@ func TestCheckForDomainRequiresValidAlignedSignature(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalDeliveryRequiresCurrentKeyDomainAndLoopback(t *testing.T) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
otherKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
message := "From: sender@example.com\r\nTo: recipient@example.com\r\nSubject: local authentication\r\n\r\noriginal body\r\n"
|
||||
for _, tc := range []struct {
|
||||
name, address, domain, selector string
|
||||
foreignKey, tampered, unsigned bool
|
||||
want bool
|
||||
}{
|
||||
{name: "ipv4_without_dns", address: "127.0.0.1", want: true},
|
||||
{name: "ipv6_without_dns", address: "::1", want: true},
|
||||
{name: "mapped_loopback", address: "::ffff:127.0.0.1", want: true},
|
||||
{name: "public_remote", address: "203.0.113.9"},
|
||||
{name: "private_remote", address: "10.0.0.9"},
|
||||
{name: "missing_remote"},
|
||||
{name: "foreign_domain", address: "127.0.0.1", domain: "other.example"},
|
||||
{name: "subdomain", address: "127.0.0.1", domain: "sub.example.com"},
|
||||
{name: "unknown_selector", address: "127.0.0.1", selector: "other"},
|
||||
{name: "wrong_key", address: "127.0.0.1", foreignKey: true},
|
||||
{name: "tampered", address: "127.0.0.1", tampered: true},
|
||||
{name: "unsigned", address: "127.0.0.1", unsigned: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
domain, selector := tc.domain, tc.selector
|
||||
if domain == "" {
|
||||
domain = "example.com"
|
||||
}
|
||||
if selector == "" {
|
||||
selector = "default"
|
||||
}
|
||||
signingKey := key
|
||||
if tc.foreignKey {
|
||||
signingKey = otherKey
|
||||
}
|
||||
data := message
|
||||
if !tc.unsigned {
|
||||
var signed bytes.Buffer
|
||||
if err := dkim.Sign(&signed, strings.NewReader(data), &dkim.SignOptions{Domain: domain, Selector: selector, Signer: signingKey}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data = signed.String()
|
||||
}
|
||||
if tc.tampered {
|
||||
data += "tampered body\r\n"
|
||||
}
|
||||
remote := &net.TCPAddr{IP: net.ParseIP(tc.address), Port: 12345}
|
||||
if got := checkLocalDelivery(nil, strings.NewReader(data), domain, "example.com", remote, key); got != tc.want {
|
||||
t.Fatalf("本机投递认证结果为 %v,期望 %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if checkLocalDelivery(nil, strings.NewReader(message), "example.com", "example.com", nil, key) {
|
||||
t.Fatal("缺少连接地址时不能通过本机认证")
|
||||
}
|
||||
if checkLocalDelivery(nil, strings.NewReader(message), "example.com", "example.com", &net.TCPAddr{IP: net.ParseIP("127.0.0.1")}, nil) {
|
||||
t.Fatal("缺少当前签名密钥时不能通过本机认证")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPrivateKeyAcceptsStrongRSAKey(t *testing.T) {
|
||||
path := writeDKIMTestKey(t, 2048)
|
||||
signer, err := loadPrivateKey(path)
|
||||
|
||||
@ -200,7 +200,12 @@ func (s *Session) Data(r io.Reader) (resultErr error) {
|
||||
dkimDomain = fromDomain
|
||||
}
|
||||
// DKIM校验
|
||||
dkimStatus = parsemail.CheckForDomain(ctx, bytes.NewReader(emailData), dkimDomain)
|
||||
if localSender {
|
||||
dkimStatus = parsemail.CheckLocalDelivery(ctx, bytes.NewReader(emailData), fromDomain, s.RemoteAddress)
|
||||
}
|
||||
if !dkimStatus {
|
||||
dkimStatus = parsemail.CheckForDomain(ctx, bytes.NewReader(emailData), dkimDomain)
|
||||
}
|
||||
|
||||
SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user