mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-10 22:19:31 +01:00
Merge pull request #928 from slingamn/dkim.4
add DKIM and email blacklist support
This commit is contained in:
commit
9f48fcedab
1
Makefile
1
Makefile
@ -22,6 +22,7 @@ test:
|
|||||||
cd irc/caps && go test . && go vet .
|
cd irc/caps && go test . && go vet .
|
||||||
cd irc/cloaks && go test . && go vet .
|
cd irc/cloaks && go test . && go vet .
|
||||||
cd irc/connection_limits && go test . && go vet .
|
cd irc/connection_limits && go test . && go vet .
|
||||||
|
cd irc/email && go test . && go vet .
|
||||||
cd irc/history && go test . && go vet .
|
cd irc/history && go test . && go vet .
|
||||||
cd irc/isupport && go test . && go vet .
|
cd irc/isupport && go test . && go vet .
|
||||||
cd irc/modes && go test . && go vet .
|
cd irc/modes && go test . && go vet .
|
||||||
|
@ -280,16 +280,24 @@ accounts:
|
|||||||
enabled-callbacks:
|
enabled-callbacks:
|
||||||
- none # no verification needed, will instantly register successfully
|
- none # no verification needed, will instantly register successfully
|
||||||
|
|
||||||
# example configuration for sending verification emails via a local mail relay
|
# example configuration for sending verification emails
|
||||||
# callbacks:
|
# callbacks:
|
||||||
# mailto:
|
# mailto:
|
||||||
# server: localhost
|
|
||||||
# port: 25
|
|
||||||
# tls:
|
|
||||||
# enabled: false
|
|
||||||
# username: ""
|
|
||||||
# password: ""
|
|
||||||
# sender: "admin@my.network"
|
# sender: "admin@my.network"
|
||||||
|
# require-tls: true
|
||||||
|
# helo-domain: "my.network" # defaults to server name if unset
|
||||||
|
# dkim:
|
||||||
|
# domain: "my.network"
|
||||||
|
# selector: "20200229"
|
||||||
|
# key-file: "dkim.pem"
|
||||||
|
# # to use an MTA/smarthost instead of sending email directly:
|
||||||
|
# # mta:
|
||||||
|
# # server: localhost
|
||||||
|
# # port: 25
|
||||||
|
# # username: "admin"
|
||||||
|
# # password: "hunter2"
|
||||||
|
# blacklist-regexes:
|
||||||
|
# # - ".*@mailinator.com"
|
||||||
|
|
||||||
# throttle account login attempts (to prevent either password guessing, or DoS
|
# throttle account login attempts (to prevent either password guessing, or DoS
|
||||||
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
|
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
|
||||||
|
1
go.mod
1
go.mod
@ -18,6 +18,7 @@ require (
|
|||||||
github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
|
github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
|
||||||
github.com/stretchr/testify v1.4.0 // indirect
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
github.com/tidwall/buntdb v1.1.2
|
github.com/tidwall/buntdb v1.1.2
|
||||||
|
github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
|
||||||
golang.org/x/text v0.3.2
|
golang.org/x/text v0.3.2
|
||||||
gopkg.in/yaml.v2 v2.2.8
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
|
2
go.sum
2
go.sum
@ -65,6 +65,8 @@ github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2K
|
|||||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
|
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
|
||||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
|
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
|
||||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
|
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
|
||||||
|
github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb h1:ilDZC+k9r67aJqSOalZLtEVLO7Cmmsq5ftfcvLirc24=
|
||||||
|
github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4=
|
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4=
|
||||||
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/smtp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/oragono/oragono/irc/caps"
|
"github.com/oragono/oragono/irc/caps"
|
||||||
"github.com/oragono/oragono/irc/connection_limits"
|
"github.com/oragono/oragono/irc/connection_limits"
|
||||||
|
"github.com/oragono/oragono/irc/email"
|
||||||
"github.com/oragono/oragono/irc/ldap"
|
"github.com/oragono/oragono/irc/ldap"
|
||||||
"github.com/oragono/oragono/irc/passwd"
|
"github.com/oragono/oragono/irc/passwd"
|
||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
@ -483,7 +484,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue)
|
code, err := am.dispatchCallback(client, account, callbackNamespace, callbackValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.Unregister(casefoldedAccount, true)
|
am.Unregister(casefoldedAccount, true)
|
||||||
return errCallbackFailed
|
return errCallbackFailed
|
||||||
@ -698,17 +699,17 @@ func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasP
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
|
func (am *AccountManager) dispatchCallback(client *Client, account string, callbackNamespace string, callbackValue string) (string, error) {
|
||||||
if callbackNamespace == "*" || callbackNamespace == "none" || callbackNamespace == "admin" {
|
if callbackNamespace == "*" || callbackNamespace == "none" || callbackNamespace == "admin" {
|
||||||
return "", nil
|
return "", nil
|
||||||
} else if callbackNamespace == "mailto" {
|
} else if callbackNamespace == "mailto" {
|
||||||
return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue)
|
return am.dispatchMailtoCallback(client, account, callbackValue)
|
||||||
} else {
|
} else {
|
||||||
return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace)
|
return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
|
func (am *AccountManager) dispatchMailtoCallback(client *Client, account string, callbackValue string) (code string, err error) {
|
||||||
config := am.server.Config().Accounts.Registration.Callbacks.Mailto
|
config := am.server.Config().Accounts.Registration.Callbacks.Mailto
|
||||||
code = utils.GenerateSecretToken()
|
code = utils.GenerateSecretToken()
|
||||||
|
|
||||||
@ -716,34 +717,27 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou
|
|||||||
if subject == "" {
|
if subject == "" {
|
||||||
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
|
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
|
||||||
}
|
}
|
||||||
messageStrings := []string{
|
|
||||||
fmt.Sprintf("From: %s\r\n", config.Sender),
|
|
||||||
fmt.Sprintf("To: %s\r\n", callbackValue),
|
|
||||||
fmt.Sprintf("Subject: %s\r\n", subject),
|
|
||||||
"\r\n", // end headers, begin message body
|
|
||||||
fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n",
|
|
||||||
fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
|
|
||||||
"\r\n",
|
|
||||||
client.t("To verify your account, issue the following command:") + "\r\n",
|
|
||||||
fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
|
|
||||||
}
|
|
||||||
|
|
||||||
var message []byte
|
var message bytes.Buffer
|
||||||
for i := 0; i < len(messageStrings); i++ {
|
fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
|
||||||
message = append(message, []byte(messageStrings[i])...)
|
fmt.Fprintf(&message, "To: %s\r\n", callbackValue)
|
||||||
}
|
if config.DKIM.Domain != "" {
|
||||||
addr := fmt.Sprintf("%s:%d", config.Server, config.Port)
|
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), config.DKIM.Domain)
|
||||||
var auth smtp.Auth
|
|
||||||
if config.Username != "" && config.Password != "" {
|
|
||||||
auth = smtp.PlainAuth("", config.Username, config.Password, config.Server)
|
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(&message, "Subject: %s\r\n", subject)
|
||||||
|
message.WriteString("\r\n") // blank line: end headers, begin message body
|
||||||
|
fmt.Fprintf(&message, client.t("Account: %s"), account)
|
||||||
|
message.WriteString("\r\n")
|
||||||
|
fmt.Fprintf(&message, client.t("Verification code: %s"), code)
|
||||||
|
message.WriteString("\r\n")
|
||||||
|
message.WriteString("\r\n")
|
||||||
|
message.WriteString(client.t("To verify your account, issue the following command:"))
|
||||||
|
message.WriteString("\r\n")
|
||||||
|
fmt.Fprintf(&message, "/MSG NickServ VERIFY %s %s\r\n", account, code)
|
||||||
|
|
||||||
// TODO: this will never send the password in plaintext over a nonlocal link,
|
err = email.SendMail(config, callbackValue, message.Bytes())
|
||||||
// but it might send the email in plaintext, regardless of the value of
|
|
||||||
// config.TLS.InsecureSkipVerify
|
|
||||||
err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", "Failed to dispatch e-mail", err.Error())
|
am.server.logger.Error("internal", "Failed to dispatch e-mail to", callbackValue, err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/oragono/oragono/irc/cloaks"
|
"github.com/oragono/oragono/irc/cloaks"
|
||||||
"github.com/oragono/oragono/irc/connection_limits"
|
"github.com/oragono/oragono/irc/connection_limits"
|
||||||
"github.com/oragono/oragono/irc/custime"
|
"github.com/oragono/oragono/irc/custime"
|
||||||
|
"github.com/oragono/oragono/irc/email"
|
||||||
"github.com/oragono/oragono/irc/isupport"
|
"github.com/oragono/oragono/irc/isupport"
|
||||||
"github.com/oragono/oragono/irc/languages"
|
"github.com/oragono/oragono/irc/languages"
|
||||||
"github.com/oragono/oragono/irc/ldap"
|
"github.com/oragono/oragono/irc/ldap"
|
||||||
@ -290,20 +291,7 @@ type AccountRegistrationConfig struct {
|
|||||||
EnabledCredentialTypes []string `yaml:"-"`
|
EnabledCredentialTypes []string `yaml:"-"`
|
||||||
VerifyTimeout custime.Duration `yaml:"verify-timeout"`
|
VerifyTimeout custime.Duration `yaml:"verify-timeout"`
|
||||||
Callbacks struct {
|
Callbacks struct {
|
||||||
Mailto struct {
|
Mailto email.MailtoConfig
|
||||||
Server string
|
|
||||||
Port int
|
|
||||||
TLS struct {
|
|
||||||
Enabled bool
|
|
||||||
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
|
|
||||||
ServerName string `yaml:"servername"`
|
|
||||||
}
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Sender string
|
|
||||||
VerifyMessageSubject string `yaml:"verify-message-subject"`
|
|
||||||
VerifyMessage string `yaml:"verify-message"`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
BcryptCost uint `yaml:"bcrypt-cost"`
|
BcryptCost uint `yaml:"bcrypt-cost"`
|
||||||
}
|
}
|
||||||
@ -975,14 +963,24 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
|
|
||||||
// hardcode this for now
|
// hardcode this for now
|
||||||
config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"}
|
config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"}
|
||||||
|
mailtoEnabled := false
|
||||||
for i, name := range config.Accounts.Registration.EnabledCallbacks {
|
for i, name := range config.Accounts.Registration.EnabledCallbacks {
|
||||||
if name == "none" {
|
if name == "none" {
|
||||||
// we store "none" as "*" internally
|
// we store "none" as "*" internally
|
||||||
config.Accounts.Registration.EnabledCallbacks[i] = "*"
|
config.Accounts.Registration.EnabledCallbacks[i] = "*"
|
||||||
|
} else if name == "mailto" {
|
||||||
|
mailtoEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Strings(config.Accounts.Registration.EnabledCallbacks)
|
sort.Strings(config.Accounts.Registration.EnabledCallbacks)
|
||||||
|
|
||||||
|
if mailtoEnabled {
|
||||||
|
err := config.Accounts.Registration.Callbacks.Mailto.Postprocess(config.Server.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.Accounts.RequireSasl.exemptedNets, err = utils.ParseNetList(config.Accounts.RequireSasl.Exempted)
|
config.Accounts.RequireSasl.exemptedNets, err = utils.ParseNetList(config.Accounts.RequireSasl.Exempted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Could not parse require-sasl exempted nets: %v", err.Error())
|
return nil, fmt.Errorf("Could not parse require-sasl exempted nets: %v", err.Error())
|
||||||
|
54
irc/email/dkim.go
Normal file
54
irc/email/dkim.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
dkim "github.com/toorop/go-dkim"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMissingFields = errors.New("DKIM config is missing fields")
|
||||||
|
)
|
||||||
|
|
||||||
|
type DKIMConfig struct {
|
||||||
|
Domain string
|
||||||
|
Selector string
|
||||||
|
KeyFile string `yaml:"key-file"`
|
||||||
|
keyBytes []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dkim *DKIMConfig) Postprocess() (err error) {
|
||||||
|
if dkim.Domain != "" {
|
||||||
|
if dkim.Selector == "" || dkim.KeyFile == "" {
|
||||||
|
return ErrMissingFields
|
||||||
|
}
|
||||||
|
dkim.keyBytes, err = ioutil.ReadFile(dkim.KeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultOptions = dkim.SigOptions{
|
||||||
|
Version: 1,
|
||||||
|
Canonicalization: "relaxed/relaxed",
|
||||||
|
Algo: "rsa-sha256",
|
||||||
|
Headers: []string{"from", "to", "subject", "message-id"},
|
||||||
|
BodyLength: 0,
|
||||||
|
QueryMethods: []string{"dns/txt"},
|
||||||
|
AddSignatureTimestamp: true,
|
||||||
|
SignatureExpireIn: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) {
|
||||||
|
options := defaultOptions
|
||||||
|
options.PrivateKey = dkimConfig.keyBytes
|
||||||
|
options.Domain = dkimConfig.Domain
|
||||||
|
options.Selector = dkimConfig.Selector
|
||||||
|
err = dkim.Sign(&message, options)
|
||||||
|
return message, err
|
||||||
|
}
|
124
irc/email/email.go
Normal file
124
irc/email/email.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/oragono/oragono/irc/smtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBlacklistedAddress = errors.New("Email address is blacklisted")
|
||||||
|
ErrInvalidAddress = errors.New("Email address is blacklisted")
|
||||||
|
ErrNoMXRecord = errors.New("Couldn't resolve MX record")
|
||||||
|
)
|
||||||
|
|
||||||
|
type MTAConfig struct {
|
||||||
|
Server string
|
||||||
|
Port int
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailtoConfig struct {
|
||||||
|
// legacy config format assumed the use of an MTA/smarthost,
|
||||||
|
// so server, port, etc. appear directly at top level
|
||||||
|
// XXX: see https://github.com/go-yaml/yaml/issues/63
|
||||||
|
MTAConfig `yaml:",inline"`
|
||||||
|
Sender string
|
||||||
|
HeloDomain string `yaml:"helo-domain"`
|
||||||
|
RequireTLS bool `yaml:"require-tls"`
|
||||||
|
VerifyMessageSubject string `yaml:"verify-message-subject"`
|
||||||
|
DKIM DKIMConfig
|
||||||
|
MTAReal MTAConfig `yaml:"mta"`
|
||||||
|
BlacklistRegexes []string `yaml:"blacklist-regexes"`
|
||||||
|
blacklistRegexes []*regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
|
||||||
|
if config.Sender == "" {
|
||||||
|
return errors.New("Invalid mailto sender address")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for MTA config fields at top level,
|
||||||
|
// copy to MTAReal if present
|
||||||
|
if config.Server != "" && config.MTAReal.Server == "" {
|
||||||
|
config.MTAReal = config.MTAConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.HeloDomain == "" {
|
||||||
|
config.HeloDomain = heloDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, reg := range config.BlacklistRegexes {
|
||||||
|
compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.blacklistRegexes = append(config.blacklistRegexes, compiled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.MTAConfig.Server != "" {
|
||||||
|
// smarthost, nothing more to validate
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.DKIM.Postprocess()
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the preferred MX record hostname, "" on error
|
||||||
|
func lookupMX(domain string) (server string) {
|
||||||
|
var minPref uint16
|
||||||
|
results, err := net.LookupMX(domain)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, result := range results {
|
||||||
|
if minPref == 0 || result.Pref < minPref {
|
||||||
|
server, minPref = result.Host, result.Pref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
||||||
|
for _, reg := range config.blacklistRegexes {
|
||||||
|
if reg.MatchString(recipient) {
|
||||||
|
return ErrBlacklistedAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DKIM.Domain != "" {
|
||||||
|
msg, err = DKIMSign(msg, config.DKIM)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var addr string
|
||||||
|
var auth smtp.Auth
|
||||||
|
if config.MTAReal.Server != "" {
|
||||||
|
addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
|
||||||
|
if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
|
||||||
|
auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idx := strings.IndexByte(recipient, '@')
|
||||||
|
if idx == -1 {
|
||||||
|
return ErrInvalidAddress
|
||||||
|
}
|
||||||
|
mx := lookupMX(recipient[idx+1:])
|
||||||
|
if mx == "" {
|
||||||
|
return ErrNoMXRecord
|
||||||
|
}
|
||||||
|
addr = fmt.Sprintf("%s:smtp", mx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return smtp.SendMail(addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg, config.RequireTLS)
|
||||||
|
}
|
27
irc/smtp/LICENSE
Normal file
27
irc/smtp/LICENSE
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
110
irc/smtp/auth.go
Normal file
110
irc/smtp/auth.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/md5"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auth is implemented by an SMTP authentication mechanism.
|
||||||
|
type Auth interface {
|
||||||
|
// Start begins an authentication with a server.
|
||||||
|
// It returns the name of the authentication protocol
|
||||||
|
// and optionally data to include in the initial AUTH message
|
||||||
|
// sent to the server. It can return proto == "" to indicate
|
||||||
|
// that the authentication should be skipped.
|
||||||
|
// If it returns a non-nil error, the SMTP client aborts
|
||||||
|
// the authentication attempt and closes the connection.
|
||||||
|
Start(server *ServerInfo) (proto string, toServer []byte, err error)
|
||||||
|
|
||||||
|
// Next continues the authentication. The server has just sent
|
||||||
|
// the fromServer data. If more is true, the server expects a
|
||||||
|
// response, which Next should return as toServer; otherwise
|
||||||
|
// Next should return toServer == nil.
|
||||||
|
// If Next returns a non-nil error, the SMTP client aborts
|
||||||
|
// the authentication attempt and closes the connection.
|
||||||
|
Next(fromServer []byte, more bool) (toServer []byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerInfo records information about an SMTP server.
|
||||||
|
type ServerInfo struct {
|
||||||
|
Name string // SMTP server name
|
||||||
|
TLS bool // using TLS, with valid certificate for Name
|
||||||
|
Auth []string // advertised authentication mechanisms
|
||||||
|
}
|
||||||
|
|
||||||
|
type plainAuth struct {
|
||||||
|
identity, username, password string
|
||||||
|
host string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlainAuth returns an Auth that implements the PLAIN authentication
|
||||||
|
// mechanism as defined in RFC 4616. The returned Auth uses the given
|
||||||
|
// username and password to authenticate to host and act as identity.
|
||||||
|
// Usually identity should be the empty string, to act as username.
|
||||||
|
//
|
||||||
|
// PlainAuth will only send the credentials if the connection is using TLS
|
||||||
|
// or is connected to localhost. Otherwise authentication will fail with an
|
||||||
|
// error, without sending the credentials.
|
||||||
|
func PlainAuth(identity, username, password, host string) Auth {
|
||||||
|
return &plainAuth{identity, username, password, host}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLocalhost(name string) bool {
|
||||||
|
return name == "localhost" || name == "127.0.0.1" || name == "::1"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
|
// Must have TLS, or else localhost server.
|
||||||
|
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
|
||||||
|
// In particular, it doesn't matter if the server advertises PLAIN auth.
|
||||||
|
// That might just be the attacker saying
|
||||||
|
// "it's ok, you can trust me with your password."
|
||||||
|
if !server.TLS && !isLocalhost(server.Name) {
|
||||||
|
return "", nil, errors.New("unencrypted connection")
|
||||||
|
}
|
||||||
|
if server.Name != a.host {
|
||||||
|
return "", nil, errors.New("wrong host name")
|
||||||
|
}
|
||||||
|
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
|
||||||
|
return "PLAIN", resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *plainAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||||
|
if more {
|
||||||
|
// We've already sent everything.
|
||||||
|
return nil, errors.New("unexpected server challenge")
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type cramMD5Auth struct {
|
||||||
|
username, secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRAMMD5Auth returns an Auth that implements the CRAM-MD5 authentication
|
||||||
|
// mechanism as defined in RFC 2195.
|
||||||
|
// The returned Auth uses the given username and secret to authenticate
|
||||||
|
// to the server using the challenge-response mechanism.
|
||||||
|
func CRAMMD5Auth(username, secret string) Auth {
|
||||||
|
return &cramMD5Auth{username, secret}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *cramMD5Auth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
|
return "CRAM-MD5", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *cramMD5Auth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||||
|
if more {
|
||||||
|
d := hmac.New(md5.New, []byte(a.secret))
|
||||||
|
d.Write(fromServer)
|
||||||
|
s := make([]byte, 0, d.Size())
|
||||||
|
return []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
433
irc/smtp/smtp.go
Normal file
433
irc/smtp/smtp.go
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
|
||||||
|
// It also implements the following extensions:
|
||||||
|
// 8BITMIME RFC 1652
|
||||||
|
// AUTH RFC 2554
|
||||||
|
// STARTTLS RFC 3207
|
||||||
|
// Additional extensions may be handled by clients.
|
||||||
|
//
|
||||||
|
// The smtp package is frozen and is not accepting new features.
|
||||||
|
// Some external packages provide more functionality. See:
|
||||||
|
//
|
||||||
|
// https://godoc.org/?q=smtp
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/textproto"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Client represents a client connection to an SMTP server.
|
||||||
|
type Client struct {
|
||||||
|
// Text is the textproto.Conn used by the Client. It is exported to allow for
|
||||||
|
// clients to add extensions.
|
||||||
|
Text *textproto.Conn
|
||||||
|
// keep a reference to the connection so it can be used to create a TLS
|
||||||
|
// connection later
|
||||||
|
conn net.Conn
|
||||||
|
// whether the Client is using TLS
|
||||||
|
tls bool
|
||||||
|
serverName string
|
||||||
|
// map of supported extensions
|
||||||
|
ext map[string]string
|
||||||
|
// supported auth mechanisms
|
||||||
|
auth []string
|
||||||
|
localName string // the name to use in HELO/EHLO
|
||||||
|
didHello bool // whether we've said HELO/EHLO
|
||||||
|
helloError error // the error from the hello
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial returns a new Client connected to an SMTP server at addr.
|
||||||
|
// The addr must include a port, as in "mail.example.com:smtp".
|
||||||
|
func Dial(addr string) (*Client, error) {
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
host, _, _ := net.SplitHostPort(addr)
|
||||||
|
return NewClient(conn, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient returns a new Client using an existing connection and host as a
|
||||||
|
// server name to be used when authenticating.
|
||||||
|
func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||||
|
text := textproto.NewConn(conn)
|
||||||
|
_, _, err := text.ReadResponse(220)
|
||||||
|
if err != nil {
|
||||||
|
text.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
|
||||||
|
_, c.tls = conn.(*tls.Conn)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection.
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return c.Text.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// hello runs a hello exchange if needed.
|
||||||
|
func (c *Client) hello() error {
|
||||||
|
if !c.didHello {
|
||||||
|
c.didHello = true
|
||||||
|
err := c.ehlo()
|
||||||
|
if err != nil {
|
||||||
|
c.helloError = c.helo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.helloError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hello sends a HELO or EHLO to the server as the given host name.
|
||||||
|
// Calling this method is only necessary if the client needs control
|
||||||
|
// over the host name used. The client will introduce itself as "localhost"
|
||||||
|
// automatically otherwise. If Hello is called, it must be called before
|
||||||
|
// any of the other methods.
|
||||||
|
func (c *Client) Hello(localName string) error {
|
||||||
|
if err := validateLine(localName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.didHello {
|
||||||
|
return errors.New("smtp: Hello called after other methods")
|
||||||
|
}
|
||||||
|
c.localName = localName
|
||||||
|
return c.hello()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmd is a convenience function that sends a command and returns the response
|
||||||
|
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
||||||
|
id, err := c.Text.Cmd(format, args...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
c.Text.StartResponse(id)
|
||||||
|
defer c.Text.EndResponse(id)
|
||||||
|
code, msg, err := c.Text.ReadResponse(expectCode)
|
||||||
|
return code, msg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// helo sends the HELO greeting to the server. It should be used only when the
|
||||||
|
// server does not support ehlo.
|
||||||
|
func (c *Client) helo() error {
|
||||||
|
c.ext = nil
|
||||||
|
_, _, err := c.cmd(250, "HELO %s", c.localName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ehlo sends the EHLO (extended hello) greeting to the server. It
|
||||||
|
// should be the preferred greeting for servers that support it.
|
||||||
|
func (c *Client) ehlo() error {
|
||||||
|
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ext := make(map[string]string)
|
||||||
|
extList := strings.Split(msg, "\n")
|
||||||
|
if len(extList) > 1 {
|
||||||
|
extList = extList[1:]
|
||||||
|
for _, line := range extList {
|
||||||
|
args := strings.SplitN(line, " ", 2)
|
||||||
|
if len(args) > 1 {
|
||||||
|
ext[args[0]] = args[1]
|
||||||
|
} else {
|
||||||
|
ext[args[0]] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mechs, ok := ext["AUTH"]; ok {
|
||||||
|
c.auth = strings.Split(mechs, " ")
|
||||||
|
}
|
||||||
|
c.ext = ext
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTLS sends the STARTTLS command and encrypts all further communication.
|
||||||
|
// Only servers that advertise the STARTTLS extension support this function.
|
||||||
|
func (c *Client) StartTLS(config *tls.Config) error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(220, "STARTTLS")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.conn = tls.Client(c.conn, config)
|
||||||
|
c.Text = textproto.NewConn(c.conn)
|
||||||
|
c.tls = true
|
||||||
|
return c.ehlo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSConnectionState returns the client's TLS connection state.
|
||||||
|
// The return values are their zero values if StartTLS did
|
||||||
|
// not succeed.
|
||||||
|
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
|
||||||
|
tc, ok := c.conn.(*tls.Conn)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return tc.ConnectionState(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks the validity of an email address on the server.
|
||||||
|
// If Verify returns nil, the address is valid. A non-nil return
|
||||||
|
// does not necessarily indicate an invalid address. Many servers
|
||||||
|
// will not verify addresses for security reasons.
|
||||||
|
func (c *Client) Verify(addr string) error {
|
||||||
|
if err := validateLine(addr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(250, "VRFY %s", addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth authenticates a client using the provided authentication mechanism.
|
||||||
|
// A failed authentication closes the connection.
|
||||||
|
// Only servers that advertise the AUTH extension support this function.
|
||||||
|
func (c *Client) Auth(a Auth) error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoding := base64.StdEncoding
|
||||||
|
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
|
||||||
|
if err != nil {
|
||||||
|
c.Quit()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
|
||||||
|
encoding.Encode(resp64, resp)
|
||||||
|
code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64)))
|
||||||
|
for err == nil {
|
||||||
|
var msg []byte
|
||||||
|
switch code {
|
||||||
|
case 334:
|
||||||
|
msg, err = encoding.DecodeString(msg64)
|
||||||
|
case 235:
|
||||||
|
// the last message isn't base64 because it isn't a challenge
|
||||||
|
msg = []byte(msg64)
|
||||||
|
default:
|
||||||
|
err = &textproto.Error{Code: code, Msg: msg64}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
resp, err = a.Next(msg, code == 334)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// abort the AUTH
|
||||||
|
c.cmd(501, "*")
|
||||||
|
c.Quit()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
|
||||||
|
encoding.Encode(resp64, resp)
|
||||||
|
code, msg64, err = c.cmd(0, string(resp64))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mail issues a MAIL command to the server using the provided email address.
|
||||||
|
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
|
||||||
|
// parameter.
|
||||||
|
// This initiates a mail transaction and is followed by one or more Rcpt calls.
|
||||||
|
func (c *Client) Mail(from string) error {
|
||||||
|
if err := validateLine(from); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmdStr := "MAIL FROM:<%s>"
|
||||||
|
if c.ext != nil {
|
||||||
|
if _, ok := c.ext["8BITMIME"]; ok {
|
||||||
|
cmdStr += " BODY=8BITMIME"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(250, cmdStr, from)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rcpt issues a RCPT command to the server using the provided email address.
|
||||||
|
// A call to Rcpt must be preceded by a call to Mail and may be followed by
|
||||||
|
// a Data call or another Rcpt call.
|
||||||
|
func (c *Client) Rcpt(to string) error {
|
||||||
|
if err := validateLine(to); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataCloser struct {
|
||||||
|
c *Client
|
||||||
|
io.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dataCloser) Close() error {
|
||||||
|
d.WriteCloser.Close()
|
||||||
|
_, _, err := d.c.Text.ReadResponse(250)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data issues a DATA command to the server and returns a writer that
|
||||||
|
// can be used to write the mail headers and body. The caller should
|
||||||
|
// close the writer before calling any more methods on c. A call to
|
||||||
|
// Data must be preceded by one or more calls to Rcpt.
|
||||||
|
func (c *Client) Data() (io.WriteCloser, error) {
|
||||||
|
_, _, err := c.cmd(354, "DATA")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dataCloser{c, c.Text.DotWriter()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var testHookStartTLS func(*tls.Config) // nil, except for tests
|
||||||
|
|
||||||
|
// SendMail connects to the server at addr, switches to TLS if
|
||||||
|
// possible, authenticates with the optional mechanism a if possible,
|
||||||
|
// and then sends an email from address from, to addresses to, with
|
||||||
|
// message msg.
|
||||||
|
// The addr must include a port, as in "mail.example.com:smtp".
|
||||||
|
//
|
||||||
|
// The addresses in the to parameter are the SMTP RCPT addresses.
|
||||||
|
//
|
||||||
|
// The msg parameter should be an RFC 822-style email with headers
|
||||||
|
// first, a blank line, and then the message body. The lines of msg
|
||||||
|
// should be CRLF terminated. The msg headers should usually include
|
||||||
|
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
|
||||||
|
// messages is accomplished by including an email address in the to
|
||||||
|
// parameter but not including it in the msg headers.
|
||||||
|
//
|
||||||
|
// The SendMail function and the net/smtp package are low-level
|
||||||
|
// mechanisms and provide no support for DKIM signing, MIME
|
||||||
|
// attachments (see the mime/multipart package), or other mail
|
||||||
|
// functionality. Higher-level packages exist outside of the standard
|
||||||
|
// library.
|
||||||
|
// XXX: modified in Oragono to add `requireTLS` and `heloDomain` arguments
|
||||||
|
func SendMail(addr string, a Auth, heloDomain string, from string, to []string, msg []byte, requireTLS bool) error {
|
||||||
|
if err := validateLine(from); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, recp := range to {
|
||||||
|
if err := validateLine(recp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c, err := Dial(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
if err = c.Hello(heloDomain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||||
|
config := &tls.Config{ServerName: c.serverName}
|
||||||
|
if testHookStartTLS != nil {
|
||||||
|
testHookStartTLS(config)
|
||||||
|
}
|
||||||
|
if err = c.StartTLS(config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if requireTLS {
|
||||||
|
return errors.New("TLS required, but not negotiated")
|
||||||
|
}
|
||||||
|
if a != nil && c.ext != nil {
|
||||||
|
if _, ok := c.ext["AUTH"]; !ok {
|
||||||
|
return errors.New("smtp: server doesn't support AUTH")
|
||||||
|
}
|
||||||
|
if err = c.Auth(a); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = c.Mail(from); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, addr := range to {
|
||||||
|
if err = c.Rcpt(addr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w, err := c.Data()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension reports whether an extension is support by the server.
|
||||||
|
// The extension name is case-insensitive. If the extension is supported,
|
||||||
|
// Extension also returns a string that contains any parameters the
|
||||||
|
// server specifies for the extension.
|
||||||
|
func (c *Client) Extension(ext string) (bool, string) {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
if c.ext == nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
ext = strings.ToUpper(ext)
|
||||||
|
param, ok := c.ext[ext]
|
||||||
|
return ok, param
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset sends the RSET command to the server, aborting the current mail
|
||||||
|
// transaction.
|
||||||
|
func (c *Client) Reset() error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(250, "RSET")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noop sends the NOOP command to the server. It does nothing but check
|
||||||
|
// that the connection to the server is okay.
|
||||||
|
func (c *Client) Noop() error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(250, "NOOP")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit sends the QUIT command and closes the connection to the server.
|
||||||
|
func (c *Client) Quit() error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(221, "QUIT")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Text.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateLine checks to see if a line has CR or LF as per RFC 5321
|
||||||
|
func validateLine(line string) error {
|
||||||
|
if strings.ContainsAny(line, "\n\r") {
|
||||||
|
return errors.New("smtp: A line must not contain CR or LF")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
22
oragono.yaml
22
oragono.yaml
@ -301,16 +301,24 @@ accounts:
|
|||||||
enabled-callbacks:
|
enabled-callbacks:
|
||||||
- none # no verification needed, will instantly register successfully
|
- none # no verification needed, will instantly register successfully
|
||||||
|
|
||||||
# example configuration for sending verification emails via a local mail relay
|
# example configuration for sending verification emails
|
||||||
# callbacks:
|
# callbacks:
|
||||||
# mailto:
|
# mailto:
|
||||||
# server: localhost
|
|
||||||
# port: 25
|
|
||||||
# tls:
|
|
||||||
# enabled: false
|
|
||||||
# username: ""
|
|
||||||
# password: ""
|
|
||||||
# sender: "admin@my.network"
|
# sender: "admin@my.network"
|
||||||
|
# require-tls: true
|
||||||
|
# helo-domain: "my.network" # defaults to server name if unset
|
||||||
|
# dkim:
|
||||||
|
# domain: "my.network"
|
||||||
|
# selector: "20200229"
|
||||||
|
# key-file: "dkim.pem"
|
||||||
|
# # to use an MTA/smarthost instead of sending email directly:
|
||||||
|
# # mta:
|
||||||
|
# # server: localhost
|
||||||
|
# # port: 25
|
||||||
|
# # username: "admin"
|
||||||
|
# # password: "hunter2"
|
||||||
|
# blacklist-regexes:
|
||||||
|
# # - ".*@mailinator.com"
|
||||||
|
|
||||||
# throttle account login attempts (to prevent either password guessing, or DoS
|
# throttle account login attempts (to prevent either password guessing, or DoS
|
||||||
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
|
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
|
||||||
|
24
vendor/github.com/toorop/go-dkim/.gitignore
generated
vendored
Normal file
24
vendor/github.com/toorop/go-dkim/.gitignore
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
22
vendor/github.com/toorop/go-dkim/LICENSE
generated
vendored
Normal file
22
vendor/github.com/toorop/go-dkim/LICENSE
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Stéphane Depierrepont
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
56
vendor/github.com/toorop/go-dkim/README.md
generated
vendored
Normal file
56
vendor/github.com/toorop/go-dkim/README.md
generated
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# go-dkim
|
||||||
|
DKIM package for Golang
|
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/github.com/toorop/go-dkim?status.svg)](https://godoc.org/github.com/toorop/go-dkim)
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
### Install
|
||||||
|
```
|
||||||
|
go get github.com/toorop/go-dkim
|
||||||
|
```
|
||||||
|
Warning: you need to use Go 1.4.2-master or 1.4.3 (when it will be available)
|
||||||
|
see https://github.com/golang/go/issues/10482 fro more info.
|
||||||
|
|
||||||
|
### Sign email
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
dkim "github.com/toorop/go-dkim"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main(){
|
||||||
|
// email is the email to sign (byte slice)
|
||||||
|
// privateKey the private key (pem encoded, byte slice )
|
||||||
|
options := dkim.NewSigOptions()
|
||||||
|
options.PrivateKey = privateKey
|
||||||
|
options.Domain = "mydomain.tld"
|
||||||
|
options.Selector = "myselector"
|
||||||
|
options.SignatureExpireIn = 3600
|
||||||
|
options.BodyLength = 50
|
||||||
|
options.Headers = []string{"from", "date", "mime-version", "received", "received"}
|
||||||
|
options.AddSignatureTimestamp = true
|
||||||
|
options.Canonicalization = "relaxed/relaxed"
|
||||||
|
err := dkim.Sign(&email, options)
|
||||||
|
// handle err..
|
||||||
|
|
||||||
|
// And... that's it, 'email' is signed ! Amazing© !!!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
dkim "github.com/toorop/go-dkim"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main(){
|
||||||
|
// email is the email to verify (byte slice)
|
||||||
|
status, err := Verify(&email)
|
||||||
|
// handle status, err (see godoc for status)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
- [ ] handle z tag (copied header fields used for diagnostic use)
|
557
vendor/github.com/toorop/go-dkim/dkim.go
generated
vendored
Normal file
557
vendor/github.com/toorop/go-dkim/dkim.go
generated
vendored
Normal file
@ -0,0 +1,557 @@
|
|||||||
|
// Package dkim provides tools for signing and verify a email according to RFC 6376
|
||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/list"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"hash"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CRLF = "\r\n"
|
||||||
|
TAB = " "
|
||||||
|
FWS = CRLF + TAB
|
||||||
|
MaxHeaderLineLength = 70
|
||||||
|
)
|
||||||
|
|
||||||
|
type verifyOutput int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SUCCESS verifyOutput = 1 + iota
|
||||||
|
PERMFAIL
|
||||||
|
TEMPFAIL
|
||||||
|
NOTSIGNED
|
||||||
|
TESTINGSUCCESS
|
||||||
|
TESTINGPERMFAIL
|
||||||
|
TESTINGTEMPFAIL
|
||||||
|
)
|
||||||
|
|
||||||
|
// sigOptions represents signing options
|
||||||
|
type SigOptions struct {
|
||||||
|
|
||||||
|
// DKIM version (default 1)
|
||||||
|
Version uint
|
||||||
|
|
||||||
|
// Private key used for signing (required)
|
||||||
|
PrivateKey []byte
|
||||||
|
|
||||||
|
// Domain (required)
|
||||||
|
Domain string
|
||||||
|
|
||||||
|
// Selector (required)
|
||||||
|
Selector string
|
||||||
|
|
||||||
|
// The Agent of User IDentifier
|
||||||
|
Auid string
|
||||||
|
|
||||||
|
// Message canonicalization (plain-text; OPTIONAL, default is
|
||||||
|
// "simple/simple"). This tag informs the Verifier of the type of
|
||||||
|
// canonicalization used to prepare the message for signing.
|
||||||
|
Canonicalization string
|
||||||
|
|
||||||
|
// The algorithm used to generate the signature
|
||||||
|
//"rsa-sha1" or "rsa-sha256"
|
||||||
|
Algo string
|
||||||
|
|
||||||
|
// Signed header fields
|
||||||
|
Headers []string
|
||||||
|
|
||||||
|
// Body length count( if set to 0 this tag is ommited in Dkim header)
|
||||||
|
BodyLength uint
|
||||||
|
|
||||||
|
// Query Methods used to retrieve the public key
|
||||||
|
QueryMethods []string
|
||||||
|
|
||||||
|
// Add a signature timestamp
|
||||||
|
AddSignatureTimestamp bool
|
||||||
|
|
||||||
|
// Time validity of the signature (0=never)
|
||||||
|
SignatureExpireIn uint64
|
||||||
|
|
||||||
|
// CopiedHeaderFileds
|
||||||
|
CopiedHeaderFields []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigOptions returns new sigoption with some defaults value
|
||||||
|
func NewSigOptions() SigOptions {
|
||||||
|
return SigOptions{
|
||||||
|
Version: 1,
|
||||||
|
Canonicalization: "simple/simple",
|
||||||
|
Algo: "rsa-sha256",
|
||||||
|
Headers: []string{"from"},
|
||||||
|
BodyLength: 0,
|
||||||
|
QueryMethods: []string{"dns/txt"},
|
||||||
|
AddSignatureTimestamp: true,
|
||||||
|
SignatureExpireIn: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs an email
|
||||||
|
func Sign(email *[]byte, options SigOptions) error {
|
||||||
|
var privateKey *rsa.PrivateKey
|
||||||
|
|
||||||
|
// PrivateKey
|
||||||
|
if len(options.PrivateKey) == 0 {
|
||||||
|
return ErrSignPrivateKeyRequired
|
||||||
|
}
|
||||||
|
d, _ := pem.Decode(options.PrivateKey)
|
||||||
|
if d == nil {
|
||||||
|
return ErrCandNotParsePrivateKey
|
||||||
|
}
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(d.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return ErrCandNotParsePrivateKey
|
||||||
|
}
|
||||||
|
privateKey = key
|
||||||
|
|
||||||
|
// Domain required
|
||||||
|
if options.Domain == "" {
|
||||||
|
return ErrSignDomainRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selector required
|
||||||
|
if options.Selector == "" {
|
||||||
|
return ErrSignSelectorRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonicalization
|
||||||
|
options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Algo
|
||||||
|
options.Algo = strings.ToLower(options.Algo)
|
||||||
|
if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" {
|
||||||
|
return ErrSignBadAlgo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header must contain "from"
|
||||||
|
hasFrom := false
|
||||||
|
for i, h := range options.Headers {
|
||||||
|
h = strings.ToLower(h)
|
||||||
|
options.Headers[i] = h
|
||||||
|
if h == "from" {
|
||||||
|
hasFrom = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasFrom {
|
||||||
|
return ErrSignHeaderShouldContainsFrom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
headers, body, err := canonicalize(email, options.Canonicalization, options.Headers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
signHash := strings.Split(options.Algo, "-")
|
||||||
|
|
||||||
|
// hash body
|
||||||
|
bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get dkim header base
|
||||||
|
dkimHeader := newDkimHeaderBySigOptions(options)
|
||||||
|
dHeader := dkimHeader.getHeaderBaseForSigning(bodyHash)
|
||||||
|
|
||||||
|
canonicalizations := strings.Split(options.Canonicalization, "/")
|
||||||
|
dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
headers = append(headers, []byte(dHeaderCanonicalized)...)
|
||||||
|
headers = bytes.TrimRight(headers, " \r\n")
|
||||||
|
|
||||||
|
// sign
|
||||||
|
sig, err := getSignature(&headers, privateKey, signHash[1])
|
||||||
|
|
||||||
|
// add to DKIM-Header
|
||||||
|
subh := ""
|
||||||
|
l := len(subh)
|
||||||
|
for _, c := range sig {
|
||||||
|
subh += string(c)
|
||||||
|
l++
|
||||||
|
if l >= MaxHeaderLineLength {
|
||||||
|
dHeader += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
l = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dHeader += subh + CRLF
|
||||||
|
*email = append([]byte(dHeader), *email...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify verifies an email an return
|
||||||
|
// state: SUCCESS or PERMFAIL or TEMPFAIL, TESTINGSUCCESS, TESTINGPERMFAIL
|
||||||
|
// TESTINGTEMPFAIL or NOTSIGNED
|
||||||
|
// error: if an error occurs during verification
|
||||||
|
func Verify(email *[]byte, opts ...DNSOpt) (verifyOutput, error) {
|
||||||
|
// parse email
|
||||||
|
dkimHeader, err := newDkimHeaderFromEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrDkimHeaderNotFound {
|
||||||
|
return NOTSIGNED, ErrDkimHeaderNotFound
|
||||||
|
}
|
||||||
|
return PERMFAIL, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do not set query method because if it's others, validation failed earlier
|
||||||
|
pubKey, verifyOutputOnError, err := NewPubKeyRespFromDNS(dkimHeader.Selector, dkimHeader.Domain, opts...)
|
||||||
|
if err != nil {
|
||||||
|
// fix https://github.com/toorop/go-dkim/issues/1
|
||||||
|
//return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting)
|
||||||
|
return verifyOutputOnError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers)
|
||||||
|
if err != nil {
|
||||||
|
return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting)
|
||||||
|
}
|
||||||
|
sigHash := strings.Split(dkimHeader.Algorithm, "-")
|
||||||
|
// check if hash algo are compatible
|
||||||
|
compatible := false
|
||||||
|
for _, algo := range pubKey.HashAlgo {
|
||||||
|
if sigHash[1] == algo {
|
||||||
|
compatible = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !compatible {
|
||||||
|
return getVerifyOutput(PERMFAIL, ErrVerifyInappropriateHashAlgo, pubKey.FlagTesting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expired ?
|
||||||
|
if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() {
|
||||||
|
return getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//println("|" + string(body) + "|")
|
||||||
|
// get body hash
|
||||||
|
bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength)
|
||||||
|
if err != nil {
|
||||||
|
return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting)
|
||||||
|
}
|
||||||
|
//println(bodyHash)
|
||||||
|
if bodyHash != dkimHeader.BodyHash {
|
||||||
|
return getVerifyOutput(PERMFAIL, ErrVerifyBodyHash, pubKey.FlagTesting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute sig
|
||||||
|
dkimHeaderCano, err := canonicalizeHeader(dkimHeader.RawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0])
|
||||||
|
if err != nil {
|
||||||
|
return getVerifyOutput(TEMPFAIL, err, pubKey.FlagTesting)
|
||||||
|
}
|
||||||
|
toSignStr := string(headers) + dkimHeaderCano
|
||||||
|
toSign := bytes.TrimRight([]byte(toSignStr), " \r\n")
|
||||||
|
|
||||||
|
err = verifySignature(toSign, dkimHeader.SignatureData, &pubKey.PubKey, sigHash[1])
|
||||||
|
if err != nil {
|
||||||
|
return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting)
|
||||||
|
}
|
||||||
|
return SUCCESS, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getVerifyOutput returns output of verify fct according to the testing flag
|
||||||
|
func getVerifyOutput(status verifyOutput, err error, flagTesting bool) (verifyOutput, error) {
|
||||||
|
if !flagTesting {
|
||||||
|
return status, err
|
||||||
|
}
|
||||||
|
switch status {
|
||||||
|
case SUCCESS:
|
||||||
|
return TESTINGSUCCESS, err
|
||||||
|
case PERMFAIL:
|
||||||
|
return TESTINGPERMFAIL, err
|
||||||
|
case TEMPFAIL:
|
||||||
|
return TESTINGTEMPFAIL, err
|
||||||
|
}
|
||||||
|
// should never happen but compilator sream whithout return
|
||||||
|
return status, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// canonicalize returns canonicalized version of header and body
|
||||||
|
func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, err error) {
|
||||||
|
body = []byte{}
|
||||||
|
rxReduceWS := regexp.MustCompile(`[ \t]+`)
|
||||||
|
|
||||||
|
rawHeaders, rawBody, err := getHeadersBody(email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
canonicalizations := strings.Split(cano, "/")
|
||||||
|
|
||||||
|
// canonicalyze header
|
||||||
|
headersList, err := getHeadersList(&rawHeaders)
|
||||||
|
|
||||||
|
// pour chaque header a conserver on traverse tous les headers dispo
|
||||||
|
// If multi instance of a field we must keep it from the bottom to the top
|
||||||
|
var match *list.Element
|
||||||
|
headersToKeepList := list.New()
|
||||||
|
|
||||||
|
for _, headerToKeep := range h {
|
||||||
|
match = nil
|
||||||
|
headerToKeepToLower := strings.ToLower(headerToKeep)
|
||||||
|
for e := headersList.Front(); e != nil; e = e.Next() {
|
||||||
|
//fmt.Printf("|%s|\n", e.Value.(string))
|
||||||
|
t := strings.Split(e.Value.(string), ":")
|
||||||
|
if strings.ToLower(t[0]) == headerToKeepToLower {
|
||||||
|
match = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match != nil {
|
||||||
|
headersToKeepList.PushBack(match.Value.(string) + "\r\n")
|
||||||
|
headersList.Remove(match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//if canonicalizations[0] == "simple" {
|
||||||
|
for e := headersToKeepList.Front(); e != nil; e = e.Next() {
|
||||||
|
cHeader, err := canonicalizeHeader(e.Value.(string), canonicalizations[0])
|
||||||
|
if err != nil {
|
||||||
|
return headers, body, err
|
||||||
|
}
|
||||||
|
headers = append(headers, []byte(cHeader)...)
|
||||||
|
}
|
||||||
|
// canonicalyze body
|
||||||
|
if canonicalizations[1] == "simple" {
|
||||||
|
// simple
|
||||||
|
// The "simple" body canonicalization algorithm ignores all empty lines
|
||||||
|
// at the end of the message body. An empty line is a line of zero
|
||||||
|
// length after removal of the line terminator. If there is no body or
|
||||||
|
// no trailing CRLF on the message body, a CRLF is added. It makes no
|
||||||
|
// other changes to the message body. In more formal terms, the
|
||||||
|
// "simple" body canonicalization algorithm converts "*CRLF" at the end
|
||||||
|
// of the body to a single "CRLF".
|
||||||
|
// Note that a completely empty or missing body is canonicalized as a
|
||||||
|
// single "CRLF"; that is, the canonicalized length will be 2 octets.
|
||||||
|
body = bytes.TrimRight(rawBody, "\r\n")
|
||||||
|
body = append(body, []byte{13, 10}...)
|
||||||
|
} else {
|
||||||
|
// relaxed
|
||||||
|
// Ignore all whitespace at the end of lines. Implementations
|
||||||
|
// MUST NOT remove the CRLF at the end of the line.
|
||||||
|
// Reduce all sequences of WSP within a line to a single SP
|
||||||
|
// character.
|
||||||
|
// Ignore all empty lines at the end of the message body. "Empty
|
||||||
|
// line" is defined in Section 3.4.3. If the body is non-empty but
|
||||||
|
// does not end with a CRLF, a CRLF is added. (For email, this is
|
||||||
|
// only possible when using extensions to SMTP or non-SMTP transport
|
||||||
|
// mechanisms.)
|
||||||
|
rawBody = rxReduceWS.ReplaceAll(rawBody, []byte(" "))
|
||||||
|
for _, line := range bytes.SplitAfter(rawBody, []byte{10}) {
|
||||||
|
line = bytes.TrimRight(line, " \r\n")
|
||||||
|
body = append(body, line...)
|
||||||
|
body = append(body, []byte{13, 10}...)
|
||||||
|
}
|
||||||
|
body = bytes.TrimRight(body, "\r\n")
|
||||||
|
body = append(body, []byte{13, 10}...)
|
||||||
|
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// canonicalizeHeader returns canonicalized version of header
|
||||||
|
func canonicalizeHeader(header string, algo string) (string, error) {
|
||||||
|
//rxReduceWS := regexp.MustCompile(`[ \t]+`)
|
||||||
|
if algo == "simple" {
|
||||||
|
// The "simple" header canonicalization algorithm does not change header
|
||||||
|
// fields in any way. Header fields MUST be presented to the signing or
|
||||||
|
// verification algorithm exactly as they are in the message being
|
||||||
|
// signed or verified. In particular, header field names MUST NOT be
|
||||||
|
// case folded and whitespace MUST NOT be changed.
|
||||||
|
return header, nil
|
||||||
|
} else if algo == "relaxed" {
|
||||||
|
// The "relaxed" header canonicalization algorithm MUST apply the
|
||||||
|
// following steps in order:
|
||||||
|
|
||||||
|
// Convert all header field names (not the header field values) to
|
||||||
|
// lowercase. For example, convert "SUBJect: AbC" to "subject: AbC".
|
||||||
|
|
||||||
|
// Unfold all header field continuation lines as described in
|
||||||
|
// [RFC5322]; in particular, lines with terminators embedded in
|
||||||
|
// continued header field values (that is, CRLF sequences followed by
|
||||||
|
// WSP) MUST be interpreted without the CRLF. Implementations MUST
|
||||||
|
// NOT remove the CRLF at the end of the header field value.
|
||||||
|
|
||||||
|
// Convert all sequences of one or more WSP characters to a single SP
|
||||||
|
// character. WSP characters here include those before and after a
|
||||||
|
// line folding boundary.
|
||||||
|
|
||||||
|
// Delete all WSP characters at the end of each unfolded header field
|
||||||
|
// value.
|
||||||
|
|
||||||
|
// Delete any WSP characters remaining before and after the colon
|
||||||
|
// separating the header field name from the header field value. The
|
||||||
|
// colon separator MUST be retained.
|
||||||
|
kv := strings.SplitN(header, ":", 2)
|
||||||
|
if len(kv) != 2 {
|
||||||
|
return header, ErrBadMailFormatHeaders
|
||||||
|
}
|
||||||
|
k := strings.ToLower(kv[0])
|
||||||
|
k = strings.TrimSpace(k)
|
||||||
|
v := removeFWS(kv[1])
|
||||||
|
//v = rxReduceWS.ReplaceAllString(v, " ")
|
||||||
|
//v = strings.TrimSpace(v)
|
||||||
|
return k + ":" + v + CRLF, nil
|
||||||
|
}
|
||||||
|
return header, ErrSignBadCanonicalization
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBodyHash return the hash (bas64encoded) of the body
|
||||||
|
func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) {
|
||||||
|
var h hash.Hash
|
||||||
|
if algo == "sha1" {
|
||||||
|
h = sha1.New()
|
||||||
|
} else {
|
||||||
|
h = sha256.New()
|
||||||
|
}
|
||||||
|
toH := *body
|
||||||
|
// if l tag (body length)
|
||||||
|
if bodyLength != 0 {
|
||||||
|
if uint(len(toH)) < bodyLength {
|
||||||
|
return "", ErrBadDKimTagLBodyTooShort
|
||||||
|
}
|
||||||
|
toH = toH[0:bodyLength]
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Write(toH)
|
||||||
|
return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSignature return signature of toSign using key
|
||||||
|
func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, error) {
|
||||||
|
var h1 hash.Hash
|
||||||
|
var h2 crypto.Hash
|
||||||
|
switch algo {
|
||||||
|
case "sha1":
|
||||||
|
h1 = sha1.New()
|
||||||
|
h2 = crypto.SHA1
|
||||||
|
break
|
||||||
|
case "sha256":
|
||||||
|
h1 = sha256.New()
|
||||||
|
h2 = crypto.SHA256
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return "", ErrVerifyInappropriateHashAlgo
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign
|
||||||
|
h1.Write(*toSign)
|
||||||
|
sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(sig), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifySignature verify signature from pubkey
|
||||||
|
func verifySignature(toSign []byte, sig64 string, key *rsa.PublicKey, algo string) error {
|
||||||
|
var h1 hash.Hash
|
||||||
|
var h2 crypto.Hash
|
||||||
|
switch algo {
|
||||||
|
case "sha1":
|
||||||
|
h1 = sha1.New()
|
||||||
|
h2 = crypto.SHA1
|
||||||
|
break
|
||||||
|
case "sha256":
|
||||||
|
h1 = sha256.New()
|
||||||
|
h2 = crypto.SHA256
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return ErrVerifyInappropriateHashAlgo
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.Write(toSign)
|
||||||
|
sig, err := base64.StdEncoding.DecodeString(sig64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeFWS removes all FWS from string
|
||||||
|
func removeFWS(in string) string {
|
||||||
|
rxReduceWS := regexp.MustCompile(`[ \t]+`)
|
||||||
|
out := strings.Replace(in, "\n", "", -1)
|
||||||
|
out = strings.Replace(out, "\r", "", -1)
|
||||||
|
out = rxReduceWS.ReplaceAllString(out, " ")
|
||||||
|
return strings.TrimSpace(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateCanonicalization validate canonicalization (c flag)
|
||||||
|
func validateCanonicalization(cano string) (string, error) {
|
||||||
|
p := strings.Split(cano, "/")
|
||||||
|
if len(p) > 2 {
|
||||||
|
return "", ErrSignBadCanonicalization
|
||||||
|
}
|
||||||
|
if len(p) == 1 {
|
||||||
|
cano = cano + "/simple"
|
||||||
|
}
|
||||||
|
for _, c := range p {
|
||||||
|
if c != "simple" && c != "relaxed" {
|
||||||
|
return "", ErrSignBadCanonicalization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cano, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHeadersList returns headers as list
|
||||||
|
func getHeadersList(rawHeader *[]byte) (*list.List, error) {
|
||||||
|
headersList := list.New()
|
||||||
|
currentHeader := []byte{}
|
||||||
|
for _, line := range bytes.SplitAfter(*rawHeader, []byte{10}) {
|
||||||
|
if line[0] == 32 || line[0] == 9 {
|
||||||
|
if len(currentHeader) == 0 {
|
||||||
|
return headersList, ErrBadMailFormatHeaders
|
||||||
|
}
|
||||||
|
currentHeader = append(currentHeader, line...)
|
||||||
|
} else {
|
||||||
|
// New header, save current if exists
|
||||||
|
if len(currentHeader) != 0 {
|
||||||
|
headersList.PushBack(string(bytes.TrimRight(currentHeader, "\r\n")))
|
||||||
|
currentHeader = []byte{}
|
||||||
|
}
|
||||||
|
currentHeader = append(currentHeader, line...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headersList.PushBack(string(currentHeader))
|
||||||
|
return headersList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHeadersBody return headers and body
|
||||||
|
func getHeadersBody(email *[]byte) ([]byte, []byte, error) {
|
||||||
|
substitutedEmail := *email
|
||||||
|
|
||||||
|
// only replace \n with \r\n when \r\n\r\n not exists
|
||||||
|
if bytes.Index(*email, []byte{13, 10, 13, 10}) < 0 {
|
||||||
|
// \n -> \r\n
|
||||||
|
substitutedEmail = bytes.Replace(*email, []byte{10}, []byte{13, 10}, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := bytes.SplitN(substitutedEmail, []byte{13, 10, 13, 10}, 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return []byte{}, []byte{}, ErrBadMailFormat
|
||||||
|
}
|
||||||
|
// Empty body
|
||||||
|
if len(parts[1]) == 0 {
|
||||||
|
parts[1] = []byte{13, 10}
|
||||||
|
}
|
||||||
|
return parts[0], parts[1], nil
|
||||||
|
}
|
545
vendor/github.com/toorop/go-dkim/dkimHeader.go
generated
vendored
Normal file
545
vendor/github.com/toorop/go-dkim/dkimHeader.go
generated
vendored
Normal file
@ -0,0 +1,545 @@
|
|||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/mail"
|
||||||
|
"net/textproto"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dkimHeader struct {
|
||||||
|
// Version This tag defines the version of DKIM
|
||||||
|
// specification that applies to the signature record.
|
||||||
|
// tag v
|
||||||
|
Version string
|
||||||
|
|
||||||
|
// The algorithm used to generate the signature..
|
||||||
|
// Verifiers MUST support "rsa-sha1" and "rsa-sha256";
|
||||||
|
// Signers SHOULD sign using "rsa-sha256".
|
||||||
|
// tag a
|
||||||
|
Algorithm string
|
||||||
|
|
||||||
|
// The signature data (base64).
|
||||||
|
// Whitespace is ignored in this value and MUST be
|
||||||
|
// ignored when reassembling the original signature.
|
||||||
|
// In particular, the signing process can safely insert
|
||||||
|
// FWS in this value in arbitrary places to conform to line-length
|
||||||
|
// limits.
|
||||||
|
// tag b
|
||||||
|
SignatureData string
|
||||||
|
|
||||||
|
// The hash of the canonicalized body part of the message as
|
||||||
|
// limited by the "l=" tag (base64; REQUIRED).
|
||||||
|
// Whitespace is ignored in this value and MUST be ignored when reassembling the original
|
||||||
|
// signature. In particular, the signing process can safely insert
|
||||||
|
// FWS in this value in arbitrary places to conform to line-length
|
||||||
|
// limits.
|
||||||
|
// tag bh
|
||||||
|
BodyHash string
|
||||||
|
|
||||||
|
// Message canonicalization (plain-text; OPTIONAL, default is
|
||||||
|
//"simple/simple"). This tag informs the Verifier of the type of
|
||||||
|
// canonicalization used to prepare the message for signing. It
|
||||||
|
// consists of two names separated by a "slash" (%d47) character,
|
||||||
|
// corresponding to the header and body canonicalization algorithms,
|
||||||
|
// respectively. These algorithms are described in Section 3.4. If
|
||||||
|
// only one algorithm is named, that algorithm is used for the header
|
||||||
|
// and "simple" is used for the body. For example, "c=relaxed" is
|
||||||
|
// treated the same as "c=relaxed/simple".
|
||||||
|
// tag c
|
||||||
|
MessageCanonicalization string
|
||||||
|
|
||||||
|
// The SDID claiming responsibility for an introduction of a message
|
||||||
|
// into the mail stream (plain-text; REQUIRED). Hence, the SDID
|
||||||
|
// value is used to form the query for the public key. The SDID MUST
|
||||||
|
// correspond to a valid DNS name under which the DKIM key record is
|
||||||
|
// published. The conventions and semantics used by a Signer to
|
||||||
|
// create and use a specific SDID are outside the scope of this
|
||||||
|
// specification, as is any use of those conventions and semantics.
|
||||||
|
// When presented with a signature that does not meet these
|
||||||
|
// requirements, Verifiers MUST consider the signature invalid.
|
||||||
|
// Internationalized domain names MUST be encoded as A-labels, as
|
||||||
|
// described in Section 2.3 of [RFC5890].
|
||||||
|
// tag d
|
||||||
|
Domain string
|
||||||
|
|
||||||
|
// Signed header fields (plain-text, but see description; REQUIRED).
|
||||||
|
// A colon-separated list of header field names that identify the
|
||||||
|
// header fields presented to the signing algorithm. The field MUST
|
||||||
|
// contain the complete list of header fields in the order presented
|
||||||
|
// to the signing algorithm. The field MAY contain names of header
|
||||||
|
// fields that do not exist when signed; nonexistent header fields do
|
||||||
|
// not contribute to the signature computation (that is, they are
|
||||||
|
// treated as the null input, including the header field name, the
|
||||||
|
// separating colon, the header field value, and any CRLF
|
||||||
|
// terminator). The field MAY contain multiple instances of a header
|
||||||
|
// field name, meaning multiple occurrences of the corresponding
|
||||||
|
// header field are included in the header hash. The field MUST NOT
|
||||||
|
// include the DKIM-Signature header field that is being created or
|
||||||
|
// verified but may include others. Folding whitespace (FWS) MAY be
|
||||||
|
// included on either side of the colon separator. Header field
|
||||||
|
// names MUST be compared against actual header field names in a
|
||||||
|
// case-insensitive manner. This list MUST NOT be empty. See
|
||||||
|
// Section 5.4 for a discussion of choosing header fields to sign and
|
||||||
|
// Section 5.4.2 for requirements when signing multiple instances of
|
||||||
|
// a single field.
|
||||||
|
// tag h
|
||||||
|
Headers []string
|
||||||
|
|
||||||
|
// The Agent or User Identifier (AUID) on behalf of which the SDID is
|
||||||
|
// taking responsibility (dkim-quoted-printable; OPTIONAL, default is
|
||||||
|
// an empty local-part followed by an "@" followed by the domain from
|
||||||
|
// the "d=" tag).
|
||||||
|
// The syntax is a standard email address where the local-part MAY be
|
||||||
|
// omitted. The domain part of the address MUST be the same as, or a
|
||||||
|
// subdomain of, the value of the "d=" tag.
|
||||||
|
// Internationalized domain names MUST be encoded as A-labels, as
|
||||||
|
// described in Section 2.3 of [RFC5890].
|
||||||
|
// tag i
|
||||||
|
Auid string
|
||||||
|
|
||||||
|
// Body length count (plain-text unsigned decimal integer; OPTIONAL,
|
||||||
|
// default is entire body). This tag informs the Verifier of the
|
||||||
|
// number of octets in the body of the email after canonicalization
|
||||||
|
// included in the cryptographic hash, starting from 0 immediately
|
||||||
|
// following the CRLF preceding the body. This value MUST NOT be
|
||||||
|
// larger than the actual number of octets in the canonicalized
|
||||||
|
// message body. See further discussion in Section 8.2.
|
||||||
|
// tag l
|
||||||
|
BodyLength uint
|
||||||
|
|
||||||
|
// A colon-separated list of query methods used to retrieve the
|
||||||
|
// public key (plain-text; OPTIONAL, default is "dns/txt"). Each
|
||||||
|
// query method is of the form "type[/options]", where the syntax and
|
||||||
|
// semantics of the options depend on the type and specified options.
|
||||||
|
// If there are multiple query mechanisms listed, the choice of query
|
||||||
|
// mechanism MUST NOT change the interpretation of the signature.
|
||||||
|
// Implementations MUST use the recognized query mechanisms in the
|
||||||
|
// order presented. Unrecognized query mechanisms MUST be ignored.
|
||||||
|
// Currently, the only valid value is "dns/txt", which defines the
|
||||||
|
// DNS TXT resource record (RR) lookup algorithm described elsewhere
|
||||||
|
// in this document. The only option defined for the "dns" query
|
||||||
|
// type is "txt", which MUST be included. Verifiers and Signers MUST
|
||||||
|
// support "dns/txt".
|
||||||
|
// tag q
|
||||||
|
QueryMethods []string
|
||||||
|
|
||||||
|
// The selector subdividing the namespace for the "d=" (domain) tag
|
||||||
|
// (plain-text; REQUIRED).
|
||||||
|
// Internationalized selector names MUST be encoded as A-labels, as
|
||||||
|
// described in Section 2.3 of [RFC5890].
|
||||||
|
// tag s
|
||||||
|
Selector string
|
||||||
|
|
||||||
|
// Signature Timestamp (plain-text unsigned decimal integer;
|
||||||
|
// RECOMMENDED, default is an unknown creation time). The time that
|
||||||
|
// this signature was created. The format is the number of seconds
|
||||||
|
// since 00:00:00 on January 1, 1970 in the UTC time zone. The value
|
||||||
|
// is expressed as an unsigned integer in decimal ASCII. This value
|
||||||
|
// is not constrained to fit into a 31- or 32-bit integer.
|
||||||
|
// Implementations SHOULD be prepared to handle values up to at least
|
||||||
|
// 10^12 (until approximately AD 200,000; this fits into 40 bits).
|
||||||
|
// To avoid denial-of-service attacks, implementations MAY consider
|
||||||
|
// any value longer than 12 digits to be infinite. Leap seconds are
|
||||||
|
// not counted. Implementations MAY ignore signatures that have a
|
||||||
|
// timestamp in the future.
|
||||||
|
// tag t
|
||||||
|
SignatureTimestamp time.Time
|
||||||
|
|
||||||
|
// Signature Expiration (plain-text unsigned decimal integer;
|
||||||
|
// RECOMMENDED, default is no expiration). The format is the same as
|
||||||
|
// in the "t=" tag, represented as an absolute date, not as a time
|
||||||
|
// delta from the signing timestamp. The value is expressed as an
|
||||||
|
// unsigned integer in decimal ASCII, with the same constraints on
|
||||||
|
// the value in the "t=" tag. Signatures MAY be considered invalid
|
||||||
|
// if the verification time at the Verifier is past the expiration
|
||||||
|
// date. The verification time should be the time that the message
|
||||||
|
// was first received at the administrative domain of the Verifier if
|
||||||
|
// that time is reliably available; otherwise, the current time
|
||||||
|
// should be used. The value of the "x=" tag MUST be greater than
|
||||||
|
// the value of the "t=" tag if both are present.
|
||||||
|
//tag x
|
||||||
|
SignatureExpiration time.Time
|
||||||
|
|
||||||
|
// Copied header fields (dkim-quoted-printable, but see description;
|
||||||
|
// OPTIONAL, default is null). A vertical-bar-separated list of
|
||||||
|
// selected header fields present when the message was signed,
|
||||||
|
// including both the field name and value. It is not required to
|
||||||
|
// include all header fields present at the time of signing. This
|
||||||
|
// field need not contain the same header fields listed in the "h="
|
||||||
|
// tag. The header field text itself must encode the vertical bar
|
||||||
|
// ("|", %x7C) character (i.e., vertical bars in the "z=" text are
|
||||||
|
// meta-characters, and any actual vertical bar characters in a
|
||||||
|
// copied header field must be encoded). Note that all whitespace
|
||||||
|
// must be encoded, including whitespace between the colon and the
|
||||||
|
// header field value. After encoding, FWS MAY be added at arbitrary
|
||||||
|
// locations in order to avoid excessively long lines; such
|
||||||
|
// whitespace is NOT part of the value of the header field and MUST
|
||||||
|
// be removed before decoding.
|
||||||
|
// The header fields referenced by the "h=" tag refer to the fields
|
||||||
|
// in the [RFC5322] header of the message, not to any copied fields
|
||||||
|
// in the "z=" tag. Copied header field values are for diagnostic
|
||||||
|
// use.
|
||||||
|
// tag z
|
||||||
|
CopiedHeaderFields []string
|
||||||
|
|
||||||
|
// HeaderMailFromDomain store the raw email address of the header Mail From
|
||||||
|
// used for verifying in case of multiple DKIM header (we will prioritise
|
||||||
|
// header with d = mail from domain)
|
||||||
|
//HeaderMailFromDomain string
|
||||||
|
|
||||||
|
// RawForsign represents the raw part (without canonicalization) of the header
|
||||||
|
// used for computint sig in verify process
|
||||||
|
RawForSign string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value
|
||||||
|
func newDkimHeaderBySigOptions(options SigOptions) *dkimHeader {
|
||||||
|
h := new(dkimHeader)
|
||||||
|
h.Version = "1"
|
||||||
|
h.Algorithm = options.Algo
|
||||||
|
h.MessageCanonicalization = options.Canonicalization
|
||||||
|
h.Domain = options.Domain
|
||||||
|
h.Headers = options.Headers
|
||||||
|
h.Auid = options.Auid
|
||||||
|
h.BodyLength = options.BodyLength
|
||||||
|
h.QueryMethods = options.QueryMethods
|
||||||
|
h.Selector = options.Selector
|
||||||
|
if options.AddSignatureTimestamp {
|
||||||
|
h.SignatureTimestamp = time.Now()
|
||||||
|
}
|
||||||
|
if options.SignatureExpireIn > 0 {
|
||||||
|
h.SignatureExpiration = time.Now().Add(time.Duration(options.SignatureExpireIn) * time.Second)
|
||||||
|
}
|
||||||
|
h.CopiedHeaderFields = options.CopiedHeaderFields
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFromEmail return a new DkimHeader by parsing an email
|
||||||
|
// Note: according to RFC 6376 an email can have multiple DKIM Header
|
||||||
|
// in this case we return the last inserted or the last with d== mail from
|
||||||
|
func newDkimHeaderFromEmail(email *[]byte) (*dkimHeader, error) {
|
||||||
|
m, err := mail.ReadMessage(bytes.NewReader(*email))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DKIM header ?
|
||||||
|
if len(m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")]) == 0 {
|
||||||
|
return nil, ErrDkimHeaderNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mail from domain
|
||||||
|
mailFromDomain := ""
|
||||||
|
mailfrom, err := mail.ParseAddress(m.Header.Get(textproto.CanonicalMIMEHeaderKey("From")))
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != "mail: no address" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t := strings.SplitAfter(mailfrom.Address, "@")
|
||||||
|
if len(t) > 1 {
|
||||||
|
mailFromDomain = strings.ToLower(t[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get raw dkim header
|
||||||
|
// we can't use m.header because header key will be converted with textproto.CanonicalMIMEHeaderKey
|
||||||
|
// ie if key in header is not DKIM-Signature but Dkim-Signature or DKIM-signature ot... other
|
||||||
|
// combination of case, verify will fail.
|
||||||
|
rawHeaders, _, err := getHeadersBody(email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrBadMailFormat
|
||||||
|
}
|
||||||
|
rawHeadersList, err := getHeadersList(&rawHeaders)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dkHeaders := []string{}
|
||||||
|
for h := rawHeadersList.Front(); h != nil; h = h.Next() {
|
||||||
|
if strings.HasPrefix(strings.ToLower(h.Value.(string)), "dkim-signature") {
|
||||||
|
dkHeaders = append(dkHeaders, h.Value.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var keep *dkimHeader
|
||||||
|
var keepErr error
|
||||||
|
//for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] {
|
||||||
|
for _, h := range dkHeaders {
|
||||||
|
parsed, err := parseDkHeader(h)
|
||||||
|
// if malformed dkim header try next
|
||||||
|
if err != nil {
|
||||||
|
keepErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Keep first dkim headers
|
||||||
|
if keep == nil {
|
||||||
|
keep = parsed
|
||||||
|
}
|
||||||
|
// if d flag == domain keep this header and return
|
||||||
|
if mailFromDomain == parsed.Domain {
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if keep == nil {
|
||||||
|
return nil, keepErr
|
||||||
|
}
|
||||||
|
return keep, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDkHeader parse raw dkim header
|
||||||
|
func parseDkHeader(header string) (dkh *dkimHeader, err error) {
|
||||||
|
dkh = new(dkimHeader)
|
||||||
|
|
||||||
|
keyVal := strings.SplitN(header, ":", 2)
|
||||||
|
|
||||||
|
t := strings.LastIndex(header, "b=")
|
||||||
|
if t == -1 {
|
||||||
|
return nil, ErrDkimHeaderBTagNotFound
|
||||||
|
}
|
||||||
|
dkh.RawForSign = header[0 : t+2]
|
||||||
|
p := strings.IndexByte(header[t:], ';')
|
||||||
|
if p != -1 {
|
||||||
|
dkh.RawForSign = dkh.RawForSign + header[t+p:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mandatory
|
||||||
|
mandatoryFlags := make(map[string]bool, 7) //(b'v', b'a', b'b', b'bh', b'd', b'h', b's')
|
||||||
|
mandatoryFlags["v"] = false
|
||||||
|
mandatoryFlags["a"] = false
|
||||||
|
mandatoryFlags["b"] = false
|
||||||
|
mandatoryFlags["bh"] = false
|
||||||
|
mandatoryFlags["d"] = false
|
||||||
|
mandatoryFlags["h"] = false
|
||||||
|
mandatoryFlags["s"] = false
|
||||||
|
|
||||||
|
// default values
|
||||||
|
dkh.MessageCanonicalization = "simple/simple"
|
||||||
|
dkh.QueryMethods = []string{"dns/txt"}
|
||||||
|
|
||||||
|
// unfold && clean
|
||||||
|
val := removeFWS(keyVal[1])
|
||||||
|
val = strings.Replace(val, " ", "", -1)
|
||||||
|
|
||||||
|
fs := strings.Split(val, ";")
|
||||||
|
for _, f := range fs {
|
||||||
|
if f == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
flagData := strings.SplitN(f, "=", 2)
|
||||||
|
|
||||||
|
// https://github.com/toorop/go-dkim/issues/2
|
||||||
|
// if flag is not in the form key=value (eg doesn't have "=")
|
||||||
|
if len(flagData) != 2 {
|
||||||
|
return nil, ErrDkimHeaderBadFormat
|
||||||
|
}
|
||||||
|
flag := strings.ToLower(strings.TrimSpace(flagData[0]))
|
||||||
|
data := strings.TrimSpace(flagData[1])
|
||||||
|
switch flag {
|
||||||
|
case "v":
|
||||||
|
if data != "1" {
|
||||||
|
return nil, ErrDkimVersionNotsupported
|
||||||
|
}
|
||||||
|
dkh.Version = data
|
||||||
|
mandatoryFlags["v"] = true
|
||||||
|
case "a":
|
||||||
|
dkh.Algorithm = strings.ToLower(data)
|
||||||
|
if dkh.Algorithm != "rsa-sha1" && dkh.Algorithm != "rsa-sha256" {
|
||||||
|
return nil, ErrSignBadAlgo
|
||||||
|
}
|
||||||
|
mandatoryFlags["a"] = true
|
||||||
|
case "b":
|
||||||
|
//dkh.SignatureData = removeFWS(data)
|
||||||
|
// remove all space
|
||||||
|
dkh.SignatureData = strings.Replace(removeFWS(data), " ", "", -1)
|
||||||
|
if len(dkh.SignatureData) != 0 {
|
||||||
|
mandatoryFlags["b"] = true
|
||||||
|
}
|
||||||
|
case "bh":
|
||||||
|
dkh.BodyHash = removeFWS(data)
|
||||||
|
if len(dkh.BodyHash) != 0 {
|
||||||
|
mandatoryFlags["bh"] = true
|
||||||
|
}
|
||||||
|
case "d":
|
||||||
|
dkh.Domain = strings.ToLower(data)
|
||||||
|
if len(dkh.Domain) != 0 {
|
||||||
|
mandatoryFlags["d"] = true
|
||||||
|
}
|
||||||
|
case "h":
|
||||||
|
data = strings.ToLower(data)
|
||||||
|
dkh.Headers = strings.Split(data, ":")
|
||||||
|
if len(dkh.Headers) != 0 {
|
||||||
|
mandatoryFlags["h"] = true
|
||||||
|
}
|
||||||
|
fromFound := false
|
||||||
|
for _, h := range dkh.Headers {
|
||||||
|
if h == "from" {
|
||||||
|
fromFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !fromFound {
|
||||||
|
return nil, ErrDkimHeaderNoFromInHTag
|
||||||
|
}
|
||||||
|
case "s":
|
||||||
|
dkh.Selector = strings.ToLower(data)
|
||||||
|
if len(dkh.Selector) != 0 {
|
||||||
|
mandatoryFlags["s"] = true
|
||||||
|
}
|
||||||
|
case "c":
|
||||||
|
dkh.MessageCanonicalization, err = validateCanonicalization(strings.ToLower(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "i":
|
||||||
|
if data != "" {
|
||||||
|
if !strings.HasSuffix(data, dkh.Domain) {
|
||||||
|
return nil, ErrDkimHeaderDomainMismatch
|
||||||
|
}
|
||||||
|
dkh.Auid = data
|
||||||
|
}
|
||||||
|
case "l":
|
||||||
|
ui, err := strconv.ParseUint(data, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dkh.BodyLength = uint(ui)
|
||||||
|
case "q":
|
||||||
|
dkh.QueryMethods = strings.Split(data, ":")
|
||||||
|
if len(dkh.QueryMethods) == 0 || strings.ToLower(dkh.QueryMethods[0]) != "dns/txt" {
|
||||||
|
return nil, errQueryMethodNotsupported
|
||||||
|
}
|
||||||
|
case "t":
|
||||||
|
ts, err := strconv.ParseInt(data, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dkh.SignatureTimestamp = time.Unix(ts, 0)
|
||||||
|
|
||||||
|
case "x":
|
||||||
|
ts, err := strconv.ParseInt(data, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dkh.SignatureExpiration = time.Unix(ts, 0)
|
||||||
|
case "z":
|
||||||
|
dkh.CopiedHeaderFields = strings.Split(data, "|")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All mandatory flags are in ?
|
||||||
|
for _, p := range mandatoryFlags {
|
||||||
|
if !p {
|
||||||
|
return nil, ErrDkimHeaderMissingRequiredTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default for i/Auid
|
||||||
|
if dkh.Auid == "" {
|
||||||
|
dkh.Auid = "@" + dkh.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaut for query method
|
||||||
|
if len(dkh.QueryMethods) == 0 {
|
||||||
|
dkh.QueryMethods = []string{"dns/text"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dkh, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeaderBase return base header for signers
|
||||||
|
// Todo: some refactoring needed...
|
||||||
|
func (d *dkimHeader) getHeaderBaseForSigning(bodyHash string) string {
|
||||||
|
h := "DKIM-Signature: v=" + d.Version + "; a=" + d.Algorithm + "; q=" + strings.Join(d.QueryMethods, ":") + "; c=" + d.MessageCanonicalization + ";" + CRLF + TAB
|
||||||
|
subh := "s=" + d.Selector + ";"
|
||||||
|
if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
}
|
||||||
|
subh += " d=" + d.Domain + ";"
|
||||||
|
|
||||||
|
// Auid
|
||||||
|
if len(d.Auid) != 0 {
|
||||||
|
if len(subh)+len(d.Auid)+4 > MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
}
|
||||||
|
subh += " i=" + d.Auid + ";"
|
||||||
|
}
|
||||||
|
|
||||||
|
/*h := "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tmail.io; i=@tmail.io;" + FWS
|
||||||
|
subh := "q=dns/txt; s=test;"*/
|
||||||
|
|
||||||
|
// signature timestamp
|
||||||
|
if !d.SignatureTimestamp.IsZero() {
|
||||||
|
ts := d.SignatureTimestamp.Unix()
|
||||||
|
if len(subh)+14 > MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
}
|
||||||
|
subh += " t=" + fmt.Sprintf("%d", ts) + ";"
|
||||||
|
}
|
||||||
|
if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expiration
|
||||||
|
if !d.SignatureExpiration.IsZero() {
|
||||||
|
ts := d.SignatureExpiration.Unix()
|
||||||
|
if len(subh)+14 > MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
}
|
||||||
|
subh += " x=" + fmt.Sprintf("%d", ts) + ";"
|
||||||
|
}
|
||||||
|
|
||||||
|
// body length
|
||||||
|
if d.BodyLength != 0 {
|
||||||
|
bodyLengthStr := fmt.Sprintf("%d", d.BodyLength)
|
||||||
|
if len(subh)+len(bodyLengthStr)+4 > MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
}
|
||||||
|
subh += " l=" + bodyLengthStr + ";"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
if len(subh)+len(d.Headers)+4 > MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
}
|
||||||
|
subh += " h="
|
||||||
|
for _, header := range d.Headers {
|
||||||
|
if len(subh)+len(header)+1 > MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
}
|
||||||
|
subh += header + ":"
|
||||||
|
}
|
||||||
|
subh = subh[:len(subh)-1] + ";"
|
||||||
|
|
||||||
|
// BodyHash
|
||||||
|
if len(subh)+5+len(bodyHash) > MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
} else {
|
||||||
|
subh += " "
|
||||||
|
}
|
||||||
|
subh += "bh="
|
||||||
|
l := len(subh)
|
||||||
|
for _, c := range bodyHash {
|
||||||
|
subh += string(c)
|
||||||
|
l++
|
||||||
|
if l >= MaxHeaderLineLength {
|
||||||
|
h += subh + FWS
|
||||||
|
subh = ""
|
||||||
|
l = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h += subh + ";" + FWS + "b="
|
||||||
|
return h
|
||||||
|
}
|
94
vendor/github.com/toorop/go-dkim/errors.go
generated
vendored
Normal file
94
vendor/github.com/toorop/go-dkim/errors.go
generated
vendored
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrSignPrivateKeyRequired when there not private key in config
|
||||||
|
ErrSignPrivateKeyRequired = errors.New("PrivateKey is required")
|
||||||
|
|
||||||
|
// ErrSignDomainRequired when there is no domain defined in config
|
||||||
|
ErrSignDomainRequired = errors.New("Domain is required")
|
||||||
|
|
||||||
|
// ErrSignSelectorRequired when there is no Selcteir defined in config
|
||||||
|
ErrSignSelectorRequired = errors.New("Selector is required")
|
||||||
|
|
||||||
|
// ErrSignHeaderShouldContainsFrom If Headers is specified it should at least contain 'from'
|
||||||
|
ErrSignHeaderShouldContainsFrom = errors.New("header must contains 'from' field")
|
||||||
|
|
||||||
|
// ErrSignBadCanonicalization If bad Canonicalization parameter
|
||||||
|
ErrSignBadCanonicalization = errors.New("bad Canonicalization parameter")
|
||||||
|
|
||||||
|
// ErrCandNotParsePrivateKey when unable to parse private key
|
||||||
|
ErrCandNotParsePrivateKey = errors.New("can not parse private key, check format (pem) and validity")
|
||||||
|
|
||||||
|
// ErrSignBadAlgo Bad algorithm
|
||||||
|
ErrSignBadAlgo = errors.New("bad algorithm. Only rsa-sha1 or rsa-sha256 are permitted")
|
||||||
|
|
||||||
|
// ErrBadMailFormat unable to parse mail
|
||||||
|
ErrBadMailFormat = errors.New("bad mail format")
|
||||||
|
|
||||||
|
// ErrBadMailFormatHeaders bad headers format (not DKIM Header)
|
||||||
|
ErrBadMailFormatHeaders = errors.New("bad mail format found in headers")
|
||||||
|
|
||||||
|
// ErrBadDKimTagLBodyTooShort bad l tag
|
||||||
|
ErrBadDKimTagLBodyTooShort = errors.New("bad tag l or bodyLength option. Body length < l value")
|
||||||
|
|
||||||
|
// ErrDkimHeaderBadFormat when errors found in DKIM header
|
||||||
|
ErrDkimHeaderBadFormat = errors.New("bad DKIM header format")
|
||||||
|
|
||||||
|
// ErrDkimHeaderNotFound when there's no DKIM-Signature header in an email we have to verify
|
||||||
|
ErrDkimHeaderNotFound = errors.New("no DKIM-Signature header field found ")
|
||||||
|
|
||||||
|
// ErrDkimHeaderBTagNotFound when there's no b tag
|
||||||
|
ErrDkimHeaderBTagNotFound = errors.New("no tag 'b' found in dkim header")
|
||||||
|
|
||||||
|
// ErrDkimHeaderNoFromInHTag when from is missing in h tag
|
||||||
|
ErrDkimHeaderNoFromInHTag = errors.New("'from' header is missing in h tag")
|
||||||
|
|
||||||
|
// ErrDkimHeaderMissingRequiredTag when a required tag is missing
|
||||||
|
ErrDkimHeaderMissingRequiredTag = errors.New("signature missing required tag")
|
||||||
|
|
||||||
|
// ErrDkimHeaderDomainMismatch if i tag is not a sub domain of d tag
|
||||||
|
ErrDkimHeaderDomainMismatch = errors.New("domain mismatch")
|
||||||
|
|
||||||
|
// ErrDkimVersionNotsupported version not supported
|
||||||
|
ErrDkimVersionNotsupported = errors.New("incompatible version")
|
||||||
|
|
||||||
|
// Query method unsupported
|
||||||
|
errQueryMethodNotsupported = errors.New("query method not supported")
|
||||||
|
|
||||||
|
// ErrVerifyBodyHash when body hash doesn't verify
|
||||||
|
ErrVerifyBodyHash = errors.New("body hash did not verify")
|
||||||
|
|
||||||
|
// ErrVerifyNoKeyForSignature no key
|
||||||
|
ErrVerifyNoKeyForSignature = errors.New("no key for verify")
|
||||||
|
|
||||||
|
// ErrVerifyKeyUnavailable when service (dns) is anavailable
|
||||||
|
ErrVerifyKeyUnavailable = errors.New("key unavailable")
|
||||||
|
|
||||||
|
// ErrVerifyTagVMustBeTheFirst if present the v tag must be the firts in the record
|
||||||
|
ErrVerifyTagVMustBeTheFirst = errors.New("pub key syntax error: v tag must be the first")
|
||||||
|
|
||||||
|
// ErrVerifyVersionMusBeDkim1 if présent flag v (version) must be DKIM1
|
||||||
|
ErrVerifyVersionMusBeDkim1 = errors.New("flag v must be set to DKIM1")
|
||||||
|
|
||||||
|
// ErrVerifyBadKeyType bad type for pub key (only rsa is accepted)
|
||||||
|
ErrVerifyBadKeyType = errors.New("bad type for key type")
|
||||||
|
|
||||||
|
// ErrVerifyRevokedKey key(s) for this selector is revoked (p is empty)
|
||||||
|
ErrVerifyRevokedKey = errors.New("revoked key")
|
||||||
|
|
||||||
|
// ErrVerifyBadKey when we can't parse pubkey
|
||||||
|
ErrVerifyBadKey = errors.New("unable to parse pub key")
|
||||||
|
|
||||||
|
// ErrVerifyNoKey when no key is found on DNS record
|
||||||
|
ErrVerifyNoKey = errors.New("no public key found in DNS TXT")
|
||||||
|
|
||||||
|
// ErrVerifySignatureHasExpired when signature has expired
|
||||||
|
ErrVerifySignatureHasExpired = errors.New("signature has expired")
|
||||||
|
|
||||||
|
// ErrVerifyInappropriateHashAlgo when h tag in pub key doesn't contain hash algo from a tag of DKIM header
|
||||||
|
ErrVerifyInappropriateHashAlgo = errors.New("inappropriate has algorithm")
|
||||||
|
)
|
181
vendor/github.com/toorop/go-dkim/pubKeyRep.go
generated
vendored
Normal file
181
vendor/github.com/toorop/go-dkim/pubKeyRep.go
generated
vendored
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PubKeyRep represents a parsed version of public key record
|
||||||
|
type PubKeyRep struct {
|
||||||
|
Version string
|
||||||
|
HashAlgo []string
|
||||||
|
KeyType string
|
||||||
|
Note string
|
||||||
|
PubKey rsa.PublicKey
|
||||||
|
ServiceType []string
|
||||||
|
FlagTesting bool // flag y
|
||||||
|
FlagIMustBeD bool // flag i
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSOptions holds settings for looking up DNS records
|
||||||
|
type DNSOptions struct {
|
||||||
|
netLookupTXT func(name string) ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSOpt represents an optional setting for looking up DNS records
|
||||||
|
type DNSOpt interface {
|
||||||
|
apply(*DNSOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dnsOpt func(*DNSOptions)
|
||||||
|
|
||||||
|
func (opt dnsOpt) apply(dnsOpts *DNSOptions) {
|
||||||
|
opt(dnsOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSOptLookupTXT sets the function to use to lookup TXT records.
|
||||||
|
//
|
||||||
|
// This should probably only be used in tests.
|
||||||
|
func DNSOptLookupTXT(netLookupTXT func(name string) ([]string, error)) DNSOpt {
|
||||||
|
return dnsOpt(func(opts *DNSOptions) {
|
||||||
|
opts.netLookupTXT = netLookupTXT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPubKeyRespFromDNS retrieves the TXT record from DNS based on the specified domain and selector
|
||||||
|
// and parses it.
|
||||||
|
func NewPubKeyRespFromDNS(selector, domain string, opts ...DNSOpt) (*PubKeyRep, verifyOutput, error) {
|
||||||
|
dnsOpts := DNSOptions{}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.apply(&dnsOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dnsOpts.netLookupTXT == nil {
|
||||||
|
dnsOpts.netLookupTXT = net.LookupTXT
|
||||||
|
}
|
||||||
|
|
||||||
|
txt, err := dnsOpts.netLookupTXT(selector + "._domainkey." + domain)
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasSuffix(err.Error(), "no such host") {
|
||||||
|
return nil, PERMFAIL, ErrVerifyNoKeyForSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, TEMPFAIL, ErrVerifyKeyUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty record
|
||||||
|
if len(txt) == 0 {
|
||||||
|
return nil, PERMFAIL, ErrVerifyNoKeyForSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsing, we keep the first record
|
||||||
|
// TODO: if there is multiple record
|
||||||
|
|
||||||
|
return NewPubKeyResp(txt[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPubKeyResp parses DKIM record (usually from DNS)
|
||||||
|
func NewPubKeyResp(dkimRecord string) (*PubKeyRep, verifyOutput, error) {
|
||||||
|
pkr := new(PubKeyRep)
|
||||||
|
pkr.Version = "DKIM1"
|
||||||
|
pkr.HashAlgo = []string{"sha1", "sha256"}
|
||||||
|
pkr.KeyType = "rsa"
|
||||||
|
pkr.FlagTesting = false
|
||||||
|
pkr.FlagIMustBeD = false
|
||||||
|
|
||||||
|
p := strings.Split(dkimRecord, ";")
|
||||||
|
for i, data := range p {
|
||||||
|
keyVal := strings.SplitN(data, "=", 2)
|
||||||
|
val := ""
|
||||||
|
if len(keyVal) > 1 {
|
||||||
|
val = strings.TrimSpace(keyVal[1])
|
||||||
|
}
|
||||||
|
switch strings.ToLower(strings.TrimSpace(keyVal[0])) {
|
||||||
|
case "v":
|
||||||
|
// RFC: is this tag is specified it MUST be the first in the record
|
||||||
|
if i != 0 {
|
||||||
|
return nil, PERMFAIL, ErrVerifyTagVMustBeTheFirst
|
||||||
|
}
|
||||||
|
pkr.Version = val
|
||||||
|
if pkr.Version != "DKIM1" {
|
||||||
|
return nil, PERMFAIL, ErrVerifyVersionMusBeDkim1
|
||||||
|
}
|
||||||
|
case "h":
|
||||||
|
p := strings.Split(strings.ToLower(val), ":")
|
||||||
|
pkr.HashAlgo = []string{}
|
||||||
|
for _, h := range p {
|
||||||
|
h = strings.TrimSpace(h)
|
||||||
|
if h == "sha1" || h == "sha256" {
|
||||||
|
pkr.HashAlgo = append(pkr.HashAlgo, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if empty switch back to default
|
||||||
|
if len(pkr.HashAlgo) == 0 {
|
||||||
|
pkr.HashAlgo = []string{"sha1", "sha256"}
|
||||||
|
}
|
||||||
|
case "k":
|
||||||
|
if strings.ToLower(val) != "rsa" {
|
||||||
|
return nil, PERMFAIL, ErrVerifyBadKeyType
|
||||||
|
}
|
||||||
|
case "n":
|
||||||
|
qp, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(val)))
|
||||||
|
if err == nil {
|
||||||
|
val = string(qp)
|
||||||
|
}
|
||||||
|
pkr.Note = val
|
||||||
|
case "p":
|
||||||
|
rawkey := val
|
||||||
|
if rawkey == "" {
|
||||||
|
return nil, PERMFAIL, ErrVerifyRevokedKey
|
||||||
|
}
|
||||||
|
un64, err := base64.StdEncoding.DecodeString(rawkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, PERMFAIL, ErrVerifyBadKey
|
||||||
|
}
|
||||||
|
pk, err := x509.ParsePKIXPublicKey(un64)
|
||||||
|
if pk, ok := pk.(*rsa.PublicKey); ok {
|
||||||
|
pkr.PubKey = *pk
|
||||||
|
}
|
||||||
|
case "s":
|
||||||
|
t := strings.Split(strings.ToLower(val), ":")
|
||||||
|
for _, tt := range t {
|
||||||
|
tt = strings.TrimSpace(tt)
|
||||||
|
switch tt {
|
||||||
|
case "*":
|
||||||
|
pkr.ServiceType = append(pkr.ServiceType, "all")
|
||||||
|
case "email":
|
||||||
|
pkr.ServiceType = append(pkr.ServiceType, tt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "t":
|
||||||
|
flags := strings.Split(strings.ToLower(val), ":")
|
||||||
|
for _, flag := range flags {
|
||||||
|
flag = strings.TrimSpace(flag)
|
||||||
|
switch flag {
|
||||||
|
case "y":
|
||||||
|
pkr.FlagTesting = true
|
||||||
|
case "s":
|
||||||
|
pkr.FlagIMustBeD = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no pubkey
|
||||||
|
if pkr.PubKey == (rsa.PublicKey{}) {
|
||||||
|
return nil, PERMFAIL, ErrVerifyNoKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// No service type
|
||||||
|
if len(pkr.ServiceType) == 0 {
|
||||||
|
pkr.ServiceType = []string{"all"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkr, SUCCESS, nil
|
||||||
|
}
|
4
vendor/github.com/toorop/go-dkim/watch
generated
vendored
Normal file
4
vendor/github.com/toorop/go-dkim/watch
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
while true
|
||||||
|
do
|
||||||
|
inotifywait -q -r -e modify,attrib,close_write,move,create,delete . && echo "--------------" && go test -v
|
||||||
|
done
|
3
vendor/modules.txt
vendored
3
vendor/modules.txt
vendored
@ -59,6 +59,9 @@ github.com/tidwall/rtree
|
|||||||
github.com/tidwall/rtree/base
|
github.com/tidwall/rtree/base
|
||||||
# github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563
|
# github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563
|
||||||
github.com/tidwall/tinyqueue
|
github.com/tidwall/tinyqueue
|
||||||
|
# github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb
|
||||||
|
## explicit
|
||||||
|
github.com/toorop/go-dkim
|
||||||
# golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
|
# golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
|
||||||
## explicit
|
## explicit
|
||||||
golang.org/x/crypto/bcrypt
|
golang.org/x/crypto/bcrypt
|
||||||
|
Loading…
Reference in New Issue
Block a user