194 lines
5.0 KiB
Go
194 lines
5.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.pcux.cn/xuans/Smail/utils/atomicfile"
|
|
securefile "git.pcux.cn/xuans/Smail/utils/file"
|
|
)
|
|
|
|
const (
|
|
maxDKIMPrivateKeySize = 64 << 10
|
|
maxDKIMPublicKeySize = 16 << 10
|
|
)
|
|
|
|
func ensureDKIMKeyPair(basePath, privatePath string) (string, error) {
|
|
if err := ensureSecureDKIMDirectory(basePath); err != nil {
|
|
return "", err
|
|
}
|
|
privateName, err := securefile.ControlledRelativePath(basePath, privatePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
publicName := filepath.Join(filepath.Dir(privateName), "dkim.public")
|
|
|
|
root, err := os.OpenRoot(basePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer root.Close()
|
|
|
|
if err := ensureSecureDKIMParent(root, filepath.Dir(privateName)); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
privateData, privateExists, err := readDKIMFile(root, privateName, maxDKIMPrivateKeySize)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if privateExists {
|
|
if privateKey, parseErr := parseDKIMPrivateKey(privateData); parseErr == nil {
|
|
publicKey, buildErr := buildDKIMPublicKey(&privateKey.PublicKey)
|
|
if buildErr != nil {
|
|
return "", buildErr
|
|
}
|
|
publicData, publicExists, readErr := readDKIMFile(root, publicName, maxDKIMPublicKeySize)
|
|
if readErr != nil {
|
|
return "", readErr
|
|
}
|
|
if !publicExists || !bytes.Equal(publicData, []byte(publicKey)) {
|
|
if err := atomicfile.WriteRoot(root, publicName, []byte(publicKey), 0600); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if err := root.Chmod(privateName, 0600); err != nil {
|
|
return "", err
|
|
}
|
|
return publicKey, nil
|
|
}
|
|
}
|
|
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
privateBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
privatePEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateBytes})
|
|
if len(privatePEM) == 0 {
|
|
return "", errors.New("DKIM private key encoding failed")
|
|
}
|
|
publicKey, err := buildDKIMPublicKey(&privateKey.PublicKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 先发布私钥;若公钥写入中断,下次启动会从私钥重新生成并修复公钥文件。
|
|
if err := atomicfile.WriteRoot(root, privateName, privatePEM, 0600); err != nil {
|
|
return "", err
|
|
}
|
|
if err := atomicfile.WriteRoot(root, publicName, []byte(publicKey), 0600); err != nil {
|
|
return "", err
|
|
}
|
|
return publicKey, nil
|
|
}
|
|
|
|
func ensureSecureDKIMDirectory(path string) error {
|
|
if err := os.MkdirAll(path, 0700); err != nil {
|
|
return err
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return errors.New("invalid DKIM directory")
|
|
}
|
|
// #nosec G302 -- DKIM 目录只允许当前服务账户访问。
|
|
return os.Chmod(path, 0700)
|
|
}
|
|
|
|
func ensureSecureDKIMParent(root *os.Root, name string) error {
|
|
if name == "." || name == "" {
|
|
return nil
|
|
}
|
|
current := ""
|
|
for _, part := range strings.Split(filepath.Clean(name), string(filepath.Separator)) {
|
|
if part == "" || part == "." || part == ".." {
|
|
return errors.New("invalid DKIM key directory")
|
|
}
|
|
current = filepath.Join(current, part)
|
|
if err := root.Mkdir(current, 0700); err != nil && !os.IsExist(err) {
|
|
return err
|
|
}
|
|
info, err := root.Lstat(current)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return errors.New("invalid DKIM key directory")
|
|
}
|
|
if err := root.Chmod(current, 0700); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readDKIMFile(root *os.Root, name string, maxSize int64) ([]byte, bool, error) {
|
|
info, err := root.Lstat(name)
|
|
if os.IsNotExist(err) {
|
|
return nil, false, nil
|
|
}
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return nil, false, errors.New("invalid DKIM key file")
|
|
}
|
|
if info.Size() <= 0 || info.Size() > maxSize {
|
|
return nil, true, nil
|
|
}
|
|
data, err := root.ReadFile(name)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
return data, true, nil
|
|
}
|
|
|
|
func parseDKIMPrivateKey(data []byte) (*rsa.PrivateKey, error) {
|
|
block, rest := pem.Decode(data)
|
|
if block == nil || block.Type != "PRIVATE KEY" || len(bytes.TrimSpace(rest)) != 0 {
|
|
return nil, errors.New("invalid DKIM private key")
|
|
}
|
|
parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
privateKey, ok := parsed.(*rsa.PrivateKey)
|
|
if !ok || privateKey.N == nil || privateKey.N.BitLen() < 2048 {
|
|
return nil, errors.New("invalid DKIM private key")
|
|
}
|
|
if err := privateKey.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return privateKey, nil
|
|
}
|
|
|
|
func buildDKIMPublicKey(publicKey *rsa.PublicKey) (string, error) {
|
|
publicBytes, err := x509.MarshalPKIXPublicKey(publicKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(publicBytes) == 0 {
|
|
return "", fmt.Errorf("DKIM public key encoding failed")
|
|
}
|
|
return strings.Join([]string{
|
|
"v=DKIM1",
|
|
"k=rsa",
|
|
"p=" + base64.StdEncoding.EncodeToString(publicBytes),
|
|
}, "; "), nil
|
|
}
|