3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-22 03:49:27 +01:00
This commit is contained in:
Shivaram Lingamneni 2020-04-05 03:48:59 -04:00
parent 6e630a0b5c
commit ee05a4324d
19 changed files with 1738 additions and 59 deletions

View File

@ -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 .

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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
} }

View File

@ -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
View 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
View 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)
}

View File

@ -316,7 +316,8 @@ var testHookStartTLS func(*tls.Config) // nil, except for tests
// attachments (see the mime/multipart package), or other mail // attachments (see the mime/multipart package), or other mail
// functionality. Higher-level packages exist outside of the standard // functionality. Higher-level packages exist outside of the standard
// library. // 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 { if err := validateLine(from); err != nil {
return err return err
} }
@ -330,7 +331,7 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
return err return err
} }
defer c.Close() defer c.Close()
if err = c.hello(); err != nil { if err = c.Hello(heloDomain); err != nil {
return err return err
} }
if ok, _ := c.Extension("STARTTLS"); ok { 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 { if err = c.StartTLS(config); err != nil {
return err return err
} }
} else if requireTLS {
return errors.New("TLS required, but not negotiated")
} }
if a != nil && c.ext != nil { if a != nil && c.ext != nil {
if _, ok := c.ext["AUTH"]; !ok { if _, ok := c.ext["AUTH"]; !ok {

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -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