修复站内回环投递依赖公网解析导致误判并补充验签回归
This commit is contained in:
parent
04c1f236f1
commit
7b44f01d15
@ -5,6 +5,7 @@ import (
|
|||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.pcux.cn/xuans/Smail/config"
|
"git.pcux.cn/xuans/Smail/config"
|
||||||
@ -14,6 +15,7 @@ import (
|
|||||||
"github.com/emersion/go-msgauth/dkim"
|
"github.com/emersion/go-msgauth/dkim"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -123,6 +125,42 @@ func Check(ctx *context.Context, mail io.Reader) bool {
|
|||||||
return CheckForDomain(ctx, mail, "")
|
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 在指定域名时要求有效签名与发件域严格一致,避免其他域签名冒用本域。
|
// CheckForDomain 在指定域名时要求有效签名与发件域严格一致,避免其他域签名冒用本域。
|
||||||
func CheckForDomain(ctx *context.Context, mail io.Reader, domain string) bool {
|
func CheckForDomain(ctx *context.Context, mail io.Reader, domain string) bool {
|
||||||
return checkForDomain(ctx, mail, domain, nil)
|
return checkForDomain(ctx, mail, domain, nil)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"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) {
|
func TestLoadPrivateKeyAcceptsStrongRSAKey(t *testing.T) {
|
||||||
path := writeDKIMTestKey(t, 2048)
|
path := writeDKIMTestKey(t, 2048)
|
||||||
signer, err := loadPrivateKey(path)
|
signer, err := loadPrivateKey(path)
|
||||||
|
|||||||
@ -200,7 +200,12 @@ func (s *Session) Data(r io.Reader) (resultErr error) {
|
|||||||
dkimDomain = fromDomain
|
dkimDomain = fromDomain
|
||||||
}
|
}
|
||||||
// DKIM校验
|
// 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)
|
SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user