Smail/backend/dto/parsemail/dkim.go
smail d7837de561
Some checks are pending
Security Checks / security (push) Waiting to run
保存模拟部署迁移与最新构建校验阶段成果
2026-09-08 18:33:21 +08:00

143 lines
3.2 KiB
Go

package parsemail
import (
"bytes"
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/utils/context"
securefile "git.pcux.cn/xuans/Smail/utils/file"
"git.pcux.cn/xuans/Smail/utils/runtimepath"
"github.com/emersion/go-msgauth/dkim"
log "github.com/sirupsen/logrus"
"io"
"os"
"strings"
)
type Dkim struct {
privateKey crypto.Signer
}
var instance *Dkim
func Init() error {
cfg := config.CurrentConfig()
if cfg == nil {
return fmt.Errorf("runtime configuration is unavailable")
}
privateKey, err := loadPrivateKey(cfg.DkimPrivateKeyPath)
if err != nil {
return fmt.Errorf("load DKIM private key: %w", err)
}
instance = &Dkim{
privateKey: privateKey,
}
return nil
}
func loadPrivateKey(path string) (crypto.Signer, error) {
basePath := runtimepath.DKIMDirectory(config.ROOT_PATH)
privateName, err := securefile.ControlledRelativePath(basePath, path)
if err != nil {
return nil, err
}
root, err := os.OpenRoot(basePath)
if err != nil {
return nil, err
}
defer root.Close()
info, err := root.Lstat(privateName)
if err != nil {
return nil, err
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > 64<<10 {
return nil, fmt.Errorf("invalid DKIM private key file")
}
data, err := root.ReadFile(privateName)
if err != nil {
return nil, err
}
block, rest := pem.Decode(data)
if block == nil || len(bytes.TrimSpace(rest)) != 0 {
return nil, fmt.Errorf("invalid DKIM private key PEM")
}
var privateKey *rsa.PrivateKey
switch strings.ToUpper(block.Type) {
case "PRIVATE KEY":
parsed, parseErr := x509.ParsePKCS8PrivateKey(block.Bytes)
if parseErr != nil {
return nil, parseErr
}
var ok bool
privateKey, ok = parsed.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("unsupported DKIM private key type")
}
case "RSA PRIVATE KEY":
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported DKIM private key type: %q", block.Type)
}
if privateKey.N == nil || privateKey.N.BitLen() < 2048 {
return nil, fmt.Errorf("DKIM RSA private key is too weak")
}
if err := privateKey.Validate(); err != nil {
return nil, err
}
return privateKey, nil
}
func (p *Dkim) Sign(msgData string) ([]byte, error) {
if p == nil || p.privateKey == nil {
return nil, fmt.Errorf("DKIM signer is not initialized")
}
domains := config.CurrentMailDomains()
if strings.TrimSpace(domains.Primary) == "" {
return nil, fmt.Errorf("DKIM domain is not configured")
}
var b bytes.Buffer
r := strings.NewReader(msgData)
options := &dkim.SignOptions{
Domain: domains.Primary,
Selector: "default",
Signer: p.privateKey,
}
if err := dkim.Sign(&b, r, options); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func Check(ctx *context.Context, mail io.Reader) bool {
verifications, err := dkim.Verify(mail)
if err != nil {
log.WithContext(ctx).Warnf("DKIM Error:%v", err)
}
if len(verifications) == 0 {
return false
}
for _, v := range verifications {
if v.Err == nil {
log.Println("Valid signature for:", v.Domain)
} else {
log.Println("Invalid signature for:", v.Domain, v.Err)
return false
}
}
return true
}