mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 02:32:39 +01:00
parent
6e630a0b5c
commit
ee05a4324d
1
Makefile
1
Makefile
@ -22,6 +22,7 @@ test:
|
||||
cd irc/caps && go test . && go vet .
|
||||
cd irc/cloaks && 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/isupport && go test . && go vet .
|
||||
cd irc/modes && go test . && go vet .
|
||||
|
@ -280,16 +280,24 @@ accounts:
|
||||
enabled-callbacks:
|
||||
- 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:
|
||||
# mailto:
|
||||
# server: localhost
|
||||
# port: 25
|
||||
# tls:
|
||||
# enabled: false
|
||||
# username: ""
|
||||
# password: ""
|
||||
# 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
|
||||
# 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/stretchr/testify v1.4.0 // indirect
|
||||
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/text v0.3.2
|
||||
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/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
|
||||
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-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4=
|
||||
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
@ -4,9 +4,9 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/connection_limits"
|
||||
"github.com/oragono/oragono/irc/email"
|
||||
"github.com/oragono/oragono/irc/ldap"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
@ -483,7 +484,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
return err
|
||||
}
|
||||
|
||||
code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue)
|
||||
code, err := am.dispatchCallback(client, account, callbackNamespace, callbackValue)
|
||||
if err != nil {
|
||||
am.Unregister(casefoldedAccount, true)
|
||||
return errCallbackFailed
|
||||
@ -698,17 +699,17 @@ func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasP
|
||||
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" {
|
||||
return "", nil
|
||||
} else if callbackNamespace == "mailto" {
|
||||
return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue)
|
||||
return am.dispatchMailtoCallback(client, account, callbackValue)
|
||||
} else {
|
||||
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
|
||||
code = utils.GenerateSecretToken()
|
||||
|
||||
@ -716,34 +717,27 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou
|
||||
if subject == "" {
|
||||
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
|
||||
for i := 0; i < len(messageStrings); i++ {
|
||||
message = append(message, []byte(messageStrings[i])...)
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%d", config.Server, config.Port)
|
||||
var auth smtp.Auth
|
||||
if config.Username != "" && config.Password != "" {
|
||||
auth = smtp.PlainAuth("", config.Username, config.Password, config.Server)
|
||||
var message bytes.Buffer
|
||||
fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
|
||||
fmt.Fprintf(&message, "To: %s\r\n", callbackValue)
|
||||
if config.DKIM.Domain != "" {
|
||||
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), config.DKIM.Domain)
|
||||
}
|
||||
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,
|
||||
// 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)
|
||||
err = email.SendMail(config, callbackValue, message.Bytes())
|
||||
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
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"github.com/oragono/oragono/irc/cloaks"
|
||||
"github.com/oragono/oragono/irc/connection_limits"
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/email"
|
||||
"github.com/oragono/oragono/irc/isupport"
|
||||
"github.com/oragono/oragono/irc/languages"
|
||||
"github.com/oragono/oragono/irc/ldap"
|
||||
@ -290,20 +291,7 @@ type AccountRegistrationConfig struct {
|
||||
EnabledCredentialTypes []string `yaml:"-"`
|
||||
VerifyTimeout custime.Duration `yaml:"verify-timeout"`
|
||||
Callbacks struct {
|
||||
Mailto struct {
|
||||
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"`
|
||||
}
|
||||
Mailto email.MailtoConfig
|
||||
}
|
||||
BcryptCost uint `yaml:"bcrypt-cost"`
|
||||
}
|
||||
@ -975,14 +963,24 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
|
||||
// hardcode this for now
|
||||
config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"}
|
||||
mailtoEnabled := false
|
||||
for i, name := range config.Accounts.Registration.EnabledCallbacks {
|
||||
if name == "none" {
|
||||
// we store "none" as "*" internally
|
||||
config.Accounts.Registration.EnabledCallbacks[i] = "*"
|
||||
} else if name == "mailto" {
|
||||
mailtoEnabled = true
|
||||
}
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
@ -316,7 +316,8 @@ var testHookStartTLS func(*tls.Config) // nil, except for tests
|
||||
// attachments (see the mime/multipart package), or other mail
|
||||
// functionality. Higher-level packages exist outside of the standard
|
||||
// library.
|
||||
func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
|
||||
// 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
|
||||
}
|
||||
@ -330,7 +331,7 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
if err = c.hello(); err != nil {
|
||||
if err = c.Hello(heloDomain); err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
@ -341,6 +342,8 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
|
||||
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 {
|
||||
|
22
oragono.yaml
22
oragono.yaml
@ -301,16 +301,24 @@ accounts:
|
||||
enabled-callbacks:
|
||||
- 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:
|
||||
# mailto:
|
||||
# server: localhost
|
||||
# port: 25
|
||||
# tls:
|
||||
# enabled: false
|
||||
# username: ""
|
||||
# password: ""
|
||||
# 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
|
||||
# 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/tinyqueue v0.0.0-20180302190814-1e39f5511563
|
||||
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
|
||||
## explicit
|
||||
golang.org/x/crypto/bcrypt
|
||||
|
Loading…
Reference in New Issue
Block a user