mirror of
https://github.com/ergochat/ergo.git
synced 2025-05-04 05:37:39 +02:00
use emersion/go-msgauth for DKIM (#2242)
Fixes #1041 (support ed25519-sha256 for DKIM)
This commit is contained in:
parent
9c3173f573
commit
68cee9e2cd
3
go.mod
3
go.mod
@ -10,7 +10,6 @@ require (
|
|||||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
|
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
|
||||||
github.com/ergochat/irc-go v0.5.0-rc2
|
github.com/ergochat/irc-go v0.5.0-rc2
|
||||||
github.com/go-sql-driver/mysql v1.7.0
|
github.com/go-sql-driver/mysql v1.7.0
|
||||||
github.com/go-test/deep v1.0.6 // indirect
|
|
||||||
github.com/gofrs/flock v0.8.1
|
github.com/gofrs/flock v0.8.1
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
|
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
|
||||||
@ -18,7 +17,6 @@ require (
|
|||||||
github.com/onsi/gomega v1.9.0 // indirect
|
github.com/onsi/gomega v1.9.0 // indirect
|
||||||
github.com/stretchr/testify v1.4.0 // indirect
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
github.com/tidwall/buntdb v1.3.2
|
github.com/tidwall/buntdb v1.3.2
|
||||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208
|
|
||||||
github.com/xdg-go/scram v1.0.2
|
github.com/xdg-go/scram v1.0.2
|
||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/term v0.28.0
|
golang.org/x/term v0.28.0
|
||||||
@ -27,6 +25,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/emersion/go-msgauth v0.6.8
|
||||||
github.com/ergochat/webpush-go/v2 v2.0.0
|
github.com/ergochat/webpush-go/v2 v2.0.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
)
|
)
|
||||||
|
6
go.sum
6
go.sum
@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
|
github.com/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A=
|
||||||
|
github.com/emersion/go-msgauth v0.6.8/go.mod h1:YDwuyTCUHu9xxmAeVj0eW4INnwB6NNZoPdLerpSxRrc=
|
||||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons=
|
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons=
|
||||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1/go.mod h1:mov+uh1DPWsltdQnOdzn08UO9GsJ3MEvhtu0Ci37fdk=
|
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1/go.mod h1:mov+uh1DPWsltdQnOdzn08UO9GsJ3MEvhtu0Ci37fdk=
|
||||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5AnBVGzTXM/yHVytt48rXBGcJGzSbms=
|
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5AnBVGzTXM/yHVytt48rXBGcJGzSbms=
|
||||||
@ -21,8 +23,6 @@ github.com/ergochat/websocket v1.4.2-oragono1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh
|
|||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
|
||||||
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
|
||||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
@ -64,8 +64,6 @@ github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
|
|||||||
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
|
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
|
||||||
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
|
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
|
||||||
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
|
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
|
||||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
|
||||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
|
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
|
||||||
|
@ -4,9 +4,18 @@
|
|||||||
package email
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
dkim "github.com/toorop/go-dkim"
|
"fmt"
|
||||||
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
dkim "github.com/emersion/go-msgauth/dkim"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -17,38 +26,77 @@ type DKIMConfig struct {
|
|||||||
Domain string
|
Domain string
|
||||||
Selector string
|
Selector string
|
||||||
KeyFile string `yaml:"key-file"`
|
KeyFile string `yaml:"key-file"`
|
||||||
keyBytes []byte
|
privKey crypto.Signer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dkim *DKIMConfig) Enabled() bool {
|
||||||
|
return dkim.Domain != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dkim *DKIMConfig) Postprocess() (err error) {
|
func (dkim *DKIMConfig) Postprocess() (err error) {
|
||||||
if dkim.Domain != "" {
|
if !dkim.Enabled() {
|
||||||
if dkim.Selector == "" || dkim.KeyFile == "" {
|
return nil
|
||||||
return ErrMissingFields
|
|
||||||
}
|
|
||||||
dkim.keyBytes, err = os.ReadFile(dkim.KeyFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dkim.Selector == "" || dkim.KeyFile == "" {
|
||||||
|
return ErrMissingFields
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes, err := os.ReadFile(dkim.KeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Could not read DKIM key file: %w", err)
|
||||||
|
}
|
||||||
|
dkim.privKey, err = parseDKIMPrivKey(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Could not parse DKIM key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultOptions = dkim.SigOptions{
|
func parseDKIMPrivKey(input []byte) (crypto.Signer, error) {
|
||||||
Version: 1,
|
if len(input) == 0 {
|
||||||
Canonicalization: "relaxed/relaxed",
|
return nil, errors.New("DKIM private key is empty")
|
||||||
Algo: "rsa-sha256",
|
}
|
||||||
Headers: []string{"from", "to", "subject", "message-id", "date"},
|
|
||||||
BodyLength: 0,
|
// raw ed25519 private key format
|
||||||
QueryMethods: []string{"dns/txt"},
|
if len(input) == ed25519.PrivateKeySize {
|
||||||
AddSignatureTimestamp: true,
|
return ed25519.PrivateKey(input), nil
|
||||||
SignatureExpireIn: 0,
|
}
|
||||||
|
|
||||||
|
d, _ := pem.Decode(input)
|
||||||
|
if d == nil {
|
||||||
|
return nil, errors.New("Invalid PEM data for DKIM private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsaKey, err := x509.ParsePKCS1PrivateKey(d.Bytes); err == nil {
|
||||||
|
return rsaKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if k, err := x509.ParsePKCS8PrivateKey(d.Bytes); err == nil {
|
||||||
|
switch key := k.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return key, nil
|
||||||
|
case ed25519.PrivateKey:
|
||||||
|
return key, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unacceptable type for DKIM private key: %T", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("No acceptable format for DKIM private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) {
|
func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) {
|
||||||
options := defaultOptions
|
options := dkim.SignOptions{
|
||||||
options.PrivateKey = dkimConfig.keyBytes
|
Domain: dkimConfig.Domain,
|
||||||
options.Domain = dkimConfig.Domain
|
Selector: dkimConfig.Selector,
|
||||||
options.Selector = dkimConfig.Selector
|
Signer: dkimConfig.privKey,
|
||||||
err = dkim.Sign(&message, options)
|
HeaderCanonicalization: dkim.CanonicalizationRelaxed,
|
||||||
return message, err
|
BodyCanonicalization: dkim.CanonicalizationRelaxed,
|
||||||
|
}
|
||||||
|
input := bytes.NewBuffer(message)
|
||||||
|
output := bytes.NewBuffer(make([]byte, 0, len(message)+1024))
|
||||||
|
err = dkim.Sign(output, input, &options)
|
||||||
|
return output.Bytes(), err
|
||||||
}
|
}
|
||||||
|
@ -233,7 +233,7 @@ func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.DKIM.Domain != "" {
|
if config.DKIM.Enabled() {
|
||||||
msg, err = DKIMSign(msg, config.DKIM)
|
msg, err = DKIMSign(msg, config.DKIM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2015 Stéphane Depierrepont
|
Copyright (c) 2017 emersion
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
199
vendor/github.com/emersion/go-msgauth/dkim/canonical.go
generated
vendored
Normal file
199
vendor/github.com/emersion/go-msgauth/dkim/canonical.go
generated
vendored
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Canonicalization is a canonicalization algorithm.
|
||||||
|
type Canonicalization string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CanonicalizationSimple Canonicalization = "simple"
|
||||||
|
CanonicalizationRelaxed = "relaxed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type canonicalizer interface {
|
||||||
|
CanonicalizeHeader(s string) string
|
||||||
|
CanonicalizeBody(w io.Writer) io.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
var canonicalizers = map[Canonicalization]canonicalizer{
|
||||||
|
CanonicalizationSimple: new(simpleCanonicalizer),
|
||||||
|
CanonicalizationRelaxed: new(relaxedCanonicalizer),
|
||||||
|
}
|
||||||
|
|
||||||
|
// crlfFixer fixes any lone LF without a preceding CR.
|
||||||
|
type crlfFixer struct {
|
||||||
|
cr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cf *crlfFixer) Fix(b []byte) []byte {
|
||||||
|
res := make([]byte, 0, len(b))
|
||||||
|
for _, ch := range b {
|
||||||
|
prevCR := cf.cr
|
||||||
|
cf.cr = false
|
||||||
|
switch ch {
|
||||||
|
case '\r':
|
||||||
|
cf.cr = true
|
||||||
|
case '\n':
|
||||||
|
if !prevCR {
|
||||||
|
res = append(res, '\r')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = append(res, ch)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type simpleCanonicalizer struct{}
|
||||||
|
|
||||||
|
func (c *simpleCanonicalizer) CanonicalizeHeader(s string) string {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type simpleBodyCanonicalizer struct {
|
||||||
|
w io.Writer
|
||||||
|
crlfBuf []byte
|
||||||
|
crlfFixer crlfFixer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *simpleBodyCanonicalizer) Write(b []byte) (int, error) {
|
||||||
|
written := len(b)
|
||||||
|
b = append(c.crlfBuf, b...)
|
||||||
|
|
||||||
|
b = c.crlfFixer.Fix(b)
|
||||||
|
|
||||||
|
end := len(b)
|
||||||
|
// If it ends with \r, maybe the next write will begin with \n
|
||||||
|
if end > 0 && b[end-1] == '\r' {
|
||||||
|
end--
|
||||||
|
}
|
||||||
|
// Keep all \r\n sequences
|
||||||
|
for end >= 2 {
|
||||||
|
prev := b[end-2]
|
||||||
|
cur := b[end-1]
|
||||||
|
if prev != '\r' || cur != '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end -= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
c.crlfBuf = b[end:]
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if end > 0 {
|
||||||
|
_, err = c.w.Write(b[:end])
|
||||||
|
}
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *simpleBodyCanonicalizer) Close() error {
|
||||||
|
// Flush crlfBuf if it ends with a single \r (without a matching \n)
|
||||||
|
if len(c.crlfBuf) > 0 && c.crlfBuf[len(c.crlfBuf)-1] == '\r' {
|
||||||
|
if _, err := c.w.Write(c.crlfBuf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.crlfBuf = nil
|
||||||
|
|
||||||
|
if _, err := c.w.Write([]byte(crlf)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *simpleCanonicalizer) CanonicalizeBody(w io.Writer) io.WriteCloser {
|
||||||
|
return &simpleBodyCanonicalizer{w: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
type relaxedCanonicalizer struct{}
|
||||||
|
|
||||||
|
func (c *relaxedCanonicalizer) CanonicalizeHeader(s string) string {
|
||||||
|
k, v, ok := strings.Cut(s, ":")
|
||||||
|
if !ok {
|
||||||
|
return strings.TrimSpace(strings.ToLower(s)) + ":" + crlf
|
||||||
|
}
|
||||||
|
|
||||||
|
k = strings.TrimSpace(strings.ToLower(k))
|
||||||
|
v = strings.Join(strings.FieldsFunc(v, func(r rune) bool {
|
||||||
|
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||||
|
}), " ")
|
||||||
|
return k + ":" + v + crlf
|
||||||
|
}
|
||||||
|
|
||||||
|
type relaxedBodyCanonicalizer struct {
|
||||||
|
w io.Writer
|
||||||
|
crlfBuf []byte
|
||||||
|
wsp bool
|
||||||
|
written bool
|
||||||
|
crlfFixer crlfFixer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *relaxedBodyCanonicalizer) Write(b []byte) (int, error) {
|
||||||
|
written := len(b)
|
||||||
|
|
||||||
|
b = c.crlfFixer.Fix(b)
|
||||||
|
|
||||||
|
canonical := make([]byte, 0, len(b))
|
||||||
|
for _, ch := range b {
|
||||||
|
if ch == ' ' || ch == '\t' {
|
||||||
|
c.wsp = true
|
||||||
|
} else if ch == '\r' || ch == '\n' {
|
||||||
|
c.wsp = false
|
||||||
|
c.crlfBuf = append(c.crlfBuf, ch)
|
||||||
|
} else {
|
||||||
|
if len(c.crlfBuf) > 0 {
|
||||||
|
canonical = append(canonical, c.crlfBuf...)
|
||||||
|
c.crlfBuf = c.crlfBuf[:0]
|
||||||
|
}
|
||||||
|
if c.wsp {
|
||||||
|
canonical = append(canonical, ' ')
|
||||||
|
c.wsp = false
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical = append(canonical, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.written && len(canonical) > 0 {
|
||||||
|
c.written = true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.w.Write(canonical)
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *relaxedBodyCanonicalizer) Close() error {
|
||||||
|
if c.written {
|
||||||
|
if _, err := c.w.Write([]byte(crlf)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *relaxedCanonicalizer) CanonicalizeBody(w io.Writer) io.WriteCloser {
|
||||||
|
return &relaxedBodyCanonicalizer{w: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
type limitedWriter struct {
|
||||||
|
W io.Writer
|
||||||
|
N int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *limitedWriter) Write(b []byte) (int, error) {
|
||||||
|
if w.N <= 0 {
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped := 0
|
||||||
|
if int64(len(b)) > w.N {
|
||||||
|
b = b[:w.N]
|
||||||
|
skipped = int(int64(len(b)) - w.N)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := w.W.Write(b)
|
||||||
|
w.N -= int64(n)
|
||||||
|
return n + skipped, err
|
||||||
|
}
|
23
vendor/github.com/emersion/go-msgauth/dkim/dkim.go
generated
vendored
Normal file
23
vendor/github.com/emersion/go-msgauth/dkim/dkim.go
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Package dkim creates and verifies DKIM signatures, as specified in RFC 6376.
|
||||||
|
//
|
||||||
|
// # FAQ
|
||||||
|
//
|
||||||
|
// Why can't I verify a [net/mail.Message] directly? A [net/mail.Message]
|
||||||
|
// header is already parsed, and whitespace characters (especially continuation
|
||||||
|
// lines) are removed. Thus, the signature computed from the parsed header is
|
||||||
|
// not the same as the one computed from the raw header.
|
||||||
|
//
|
||||||
|
// How can I publish my public key? You have to add a TXT record to your DNS
|
||||||
|
// zone. See [RFC 6376 appendix C]. You can use the dkim-keygen tool included
|
||||||
|
// in go-msgauth to generate the key and the TXT record.
|
||||||
|
//
|
||||||
|
// [RFC 6376 appendix C]: https://tools.ietf.org/html/rfc6376#appendix-C
|
||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var now = time.Now
|
||||||
|
|
||||||
|
const headerFieldName = "DKIM-Signature"
|
167
vendor/github.com/emersion/go-msgauth/dkim/header.go
generated
vendored
Normal file
167
vendor/github.com/emersion/go-msgauth/dkim/header.go
generated
vendored
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/textproto"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const crlf = "\r\n"
|
||||||
|
|
||||||
|
type header []string
|
||||||
|
|
||||||
|
func readHeader(r *bufio.Reader) (header, error) {
|
||||||
|
tr := textproto.NewReader(r)
|
||||||
|
|
||||||
|
var h header
|
||||||
|
for {
|
||||||
|
l, err := tr.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return h, fmt.Errorf("failed to read header: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(l) == 0 {
|
||||||
|
break
|
||||||
|
} else if len(h) > 0 && (l[0] == ' ' || l[0] == '\t') {
|
||||||
|
// This is a continuation line
|
||||||
|
h[len(h)-1] += l + crlf
|
||||||
|
} else {
|
||||||
|
h = append(h, l+crlf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeHeader(w io.Writer, h header) error {
|
||||||
|
for _, kv := range h {
|
||||||
|
if _, err := w.Write([]byte(kv)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := w.Write([]byte(crlf))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func foldHeaderField(kv string) string {
|
||||||
|
buf := bytes.NewBufferString(kv)
|
||||||
|
|
||||||
|
line := make([]byte, 75) // 78 - len("\r\n\s")
|
||||||
|
first := true
|
||||||
|
var fold strings.Builder
|
||||||
|
for len, err := buf.Read(line); err != io.EOF; len, err = buf.Read(line) {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
fold.WriteString("\r\n ")
|
||||||
|
}
|
||||||
|
fold.Write(line[:len])
|
||||||
|
}
|
||||||
|
|
||||||
|
return fold.String() + crlf
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHeaderField(s string) (string, string) {
|
||||||
|
key, value, _ := strings.Cut(s, ":")
|
||||||
|
return strings.TrimSpace(key), strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHeaderParams(s string) (map[string]string, error) {
|
||||||
|
pairs := strings.Split(s, ";")
|
||||||
|
params := make(map[string]string)
|
||||||
|
for _, s := range pairs {
|
||||||
|
key, value, ok := strings.Cut(s, "=")
|
||||||
|
if !ok {
|
||||||
|
if strings.TrimSpace(s) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return params, errors.New("dkim: malformed header params")
|
||||||
|
}
|
||||||
|
|
||||||
|
params[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatHeaderParams(headerFieldName string, params map[string]string) string {
|
||||||
|
keys, bvalue, bfound := sortParams(params)
|
||||||
|
|
||||||
|
s := headerFieldName + ":"
|
||||||
|
var line string
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
v := params[k]
|
||||||
|
nextLength := 3 + len(line) + len(v) + len(k)
|
||||||
|
if nextLength > 75 {
|
||||||
|
s += line + crlf
|
||||||
|
line = ""
|
||||||
|
}
|
||||||
|
line = fmt.Sprintf("%v %v=%v;", line, k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if line != "" {
|
||||||
|
s += line
|
||||||
|
}
|
||||||
|
|
||||||
|
if bfound {
|
||||||
|
bfiled := foldHeaderField(" b=" + bvalue)
|
||||||
|
s += crlf + bfiled
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortParams(params map[string]string) ([]string, string, bool) {
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
bfound := false
|
||||||
|
var bvalue string
|
||||||
|
for k := range params {
|
||||||
|
if k == "b" {
|
||||||
|
bvalue = params["b"]
|
||||||
|
bfound = true
|
||||||
|
} else {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
return keys, bvalue, bfound
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerPicker struct {
|
||||||
|
h header
|
||||||
|
picked map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeaderPicker(h header) *headerPicker {
|
||||||
|
return &headerPicker{
|
||||||
|
h: h,
|
||||||
|
picked: make(map[string]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *headerPicker) Pick(key string) string {
|
||||||
|
key = strings.ToLower(key)
|
||||||
|
|
||||||
|
at := p.picked[key]
|
||||||
|
for i := len(p.h) - 1; i >= 0; i-- {
|
||||||
|
kv := p.h[i]
|
||||||
|
k, _ := parseHeaderField(kv)
|
||||||
|
|
||||||
|
if !strings.EqualFold(k, key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if at == 0 {
|
||||||
|
p.picked[key]++
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
at--
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
184
vendor/github.com/emersion/go-msgauth/dkim/query.go
generated
vendored
Normal file
184
vendor/github.com/emersion/go-msgauth/dkim/query.go
generated
vendored
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
type verifier interface {
|
||||||
|
Public() crypto.PublicKey
|
||||||
|
Verify(hash crypto.Hash, hashed []byte, sig []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type rsaVerifier struct {
|
||||||
|
*rsa.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v rsaVerifier) Public() crypto.PublicKey {
|
||||||
|
return v.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v rsaVerifier) Verify(hash crypto.Hash, hashed, sig []byte) error {
|
||||||
|
return rsa.VerifyPKCS1v15(v.PublicKey, hash, hashed, sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ed25519Verifier struct {
|
||||||
|
ed25519.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ed25519Verifier) Public() crypto.PublicKey {
|
||||||
|
return v.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ed25519Verifier) Verify(hash crypto.Hash, hashed, sig []byte) error {
|
||||||
|
if !ed25519.Verify(v.PublicKey, hashed, sig) {
|
||||||
|
return errors.New("dkim: invalid Ed25519 signature")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type queryResult struct {
|
||||||
|
Verifier verifier
|
||||||
|
KeyAlgo string
|
||||||
|
HashAlgos []string
|
||||||
|
Notes string
|
||||||
|
Services []string
|
||||||
|
Flags []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryMethod is a DKIM query method.
|
||||||
|
type QueryMethod string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DNS TXT resource record (RR) lookup algorithm
|
||||||
|
QueryMethodDNSTXT QueryMethod = "dns/txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type txtLookupFunc func(domain string) ([]string, error)
|
||||||
|
type queryFunc func(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error)
|
||||||
|
|
||||||
|
var queryMethods = map[QueryMethod]queryFunc{
|
||||||
|
QueryMethodDNSTXT: queryDNSTXT,
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryDNSTXT(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error) {
|
||||||
|
if txtLookup == nil {
|
||||||
|
txtLookup = net.LookupTXT
|
||||||
|
}
|
||||||
|
|
||||||
|
txts, err := txtLookup(selector + "._domainkey." + domain)
|
||||||
|
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
|
||||||
|
return nil, tempFailError("key unavailable: " + err.Error())
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, permFailError("no key for signature: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// net.LookupTXT will concatenate strings contained in a single TXT record.
|
||||||
|
// In other words, net.LookupTXT returns one entry per TXT record, even if
|
||||||
|
// a record contains multiple strings.
|
||||||
|
//
|
||||||
|
// RFC 6376 section 3.6.2.2 says multiple TXT records lead to undefined
|
||||||
|
// behavior, so reject that.
|
||||||
|
switch len(txts) {
|
||||||
|
case 0:
|
||||||
|
return nil, permFailError("no valid key found")
|
||||||
|
case 1:
|
||||||
|
return parsePublicKey(txts[0])
|
||||||
|
default:
|
||||||
|
return nil, permFailError("multiple TXT records found for key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePublicKey(s string) (*queryResult, error) {
|
||||||
|
params, err := parseHeaderParams(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, permFailError("key syntax error: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(queryResult)
|
||||||
|
|
||||||
|
if v, ok := params["v"]; ok && v != "DKIM1" {
|
||||||
|
return nil, permFailError("incompatible public key version")
|
||||||
|
}
|
||||||
|
|
||||||
|
p, ok := params["p"]
|
||||||
|
if !ok {
|
||||||
|
return nil, permFailError("key syntax error: missing public key data")
|
||||||
|
}
|
||||||
|
if p == "" {
|
||||||
|
return nil, permFailError("key revoked")
|
||||||
|
}
|
||||||
|
p = strings.ReplaceAll(p, " ", "")
|
||||||
|
b, err := base64.StdEncoding.DecodeString(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, permFailError("key syntax error: " + err.Error())
|
||||||
|
}
|
||||||
|
switch params["k"] {
|
||||||
|
case "rsa", "":
|
||||||
|
pub, err := x509.ParsePKIXPublicKey(b)
|
||||||
|
if err != nil {
|
||||||
|
// RFC 6376 is inconsistent about whether RSA public keys should
|
||||||
|
// be formatted as RSAPublicKey or SubjectPublicKeyInfo.
|
||||||
|
// Erratum 3017 (https://www.rfc-editor.org/errata/eid3017) proposes
|
||||||
|
// allowing both.
|
||||||
|
pub, err = x509.ParsePKCS1PublicKey(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, permFailError("key syntax error: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rsaPub, ok := pub.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, permFailError("key syntax error: not an RSA public key")
|
||||||
|
}
|
||||||
|
// RFC 8301 section 3.2: verifiers MUST NOT consider signatures using
|
||||||
|
// RSA keys of less than 1024 bits as valid signatures.
|
||||||
|
if rsaPub.Size()*8 < 1024 {
|
||||||
|
return nil, permFailError(fmt.Sprintf("key is too short: want 1024 bits, has %v bits", rsaPub.Size()*8))
|
||||||
|
}
|
||||||
|
res.Verifier = rsaVerifier{rsaPub}
|
||||||
|
res.KeyAlgo = "rsa"
|
||||||
|
case "ed25519":
|
||||||
|
if len(b) != ed25519.PublicKeySize {
|
||||||
|
return nil, permFailError(fmt.Sprintf("invalid Ed25519 public key size: %v bytes", len(b)))
|
||||||
|
}
|
||||||
|
ed25519Pub := ed25519.PublicKey(b)
|
||||||
|
res.Verifier = ed25519Verifier{ed25519Pub}
|
||||||
|
res.KeyAlgo = "ed25519"
|
||||||
|
default:
|
||||||
|
return nil, permFailError("unsupported key algorithm")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hashesStr, ok := params["h"]; ok {
|
||||||
|
res.HashAlgos = parseTagList(hashesStr)
|
||||||
|
}
|
||||||
|
if notes, ok := params["n"]; ok {
|
||||||
|
res.Notes = notes
|
||||||
|
}
|
||||||
|
if servicesStr, ok := params["s"]; ok {
|
||||||
|
services := parseTagList(servicesStr)
|
||||||
|
|
||||||
|
hasWildcard := false
|
||||||
|
for _, s := range services {
|
||||||
|
if s == "*" {
|
||||||
|
hasWildcard = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasWildcard {
|
||||||
|
res.Services = services
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flagsStr, ok := params["t"]; ok {
|
||||||
|
res.Flags = parseTagList(flagsStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
346
vendor/github.com/emersion/go-msgauth/dkim/sign.go
generated
vendored
Normal file
346
vendor/github.com/emersion/go-msgauth/dkim/sign.go
generated
vendored
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
var randReader io.Reader = rand.Reader
|
||||||
|
|
||||||
|
// SignOptions is used to configure Sign. Domain, Selector and Signer are
|
||||||
|
// mandatory.
|
||||||
|
type SignOptions struct {
|
||||||
|
// The SDID claiming responsibility for an introduction of a message into the
|
||||||
|
// mail stream. 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.
|
||||||
|
//
|
||||||
|
// This can't be empty.
|
||||||
|
Domain string
|
||||||
|
// The selector subdividing the namespace for the domain.
|
||||||
|
//
|
||||||
|
// This can't be empty.
|
||||||
|
Selector string
|
||||||
|
// The Agent or User Identifier (AUID) on behalf of which the SDID is taking
|
||||||
|
// responsibility.
|
||||||
|
//
|
||||||
|
// This is optional.
|
||||||
|
Identifier string
|
||||||
|
|
||||||
|
// The key used to sign the message.
|
||||||
|
//
|
||||||
|
// Supported Signer.Public() values are *rsa.PublicKey and
|
||||||
|
// ed25519.PublicKey.
|
||||||
|
Signer crypto.Signer
|
||||||
|
// The hash algorithm used to sign the message. If zero, a default hash will
|
||||||
|
// be chosen.
|
||||||
|
//
|
||||||
|
// The only supported hash algorithm is crypto.SHA256.
|
||||||
|
Hash crypto.Hash
|
||||||
|
|
||||||
|
// Header and body canonicalization algorithms.
|
||||||
|
//
|
||||||
|
// If empty, CanonicalizationSimple is used.
|
||||||
|
HeaderCanonicalization Canonicalization
|
||||||
|
BodyCanonicalization Canonicalization
|
||||||
|
|
||||||
|
// A list of header fields to include in the signature. If nil, all headers
|
||||||
|
// will be included. If not nil, "From" MUST be in the list.
|
||||||
|
//
|
||||||
|
// See RFC 6376 section 5.4.1 for recommended header fields.
|
||||||
|
HeaderKeys []string
|
||||||
|
|
||||||
|
// The expiration time. A zero value means no expiration.
|
||||||
|
Expiration time.Time
|
||||||
|
|
||||||
|
// A list of query methods used to retrieve the public key.
|
||||||
|
//
|
||||||
|
// If nil, it is implicitly defined as QueryMethodDNSTXT.
|
||||||
|
QueryMethods []QueryMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signer generates a DKIM signature.
|
||||||
|
//
|
||||||
|
// The whole message header and body must be written to the Signer. Close should
|
||||||
|
// always be called (either after the whole message has been written, or after
|
||||||
|
// an error occurred and the signer won't be used anymore). Close may return an
|
||||||
|
// error in case signing fails.
|
||||||
|
//
|
||||||
|
// After a successful Close, Signature can be called to retrieve the
|
||||||
|
// DKIM-Signature header field that the caller should prepend to the message.
|
||||||
|
type Signer struct {
|
||||||
|
pw *io.PipeWriter
|
||||||
|
done <-chan error
|
||||||
|
sigParams map[string]string // only valid after done received nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigner creates a new signer. It returns an error if SignOptions is
|
||||||
|
// invalid.
|
||||||
|
func NewSigner(options *SignOptions) (*Signer, error) {
|
||||||
|
if options == nil {
|
||||||
|
return nil, fmt.Errorf("dkim: no options specified")
|
||||||
|
}
|
||||||
|
if options.Domain == "" {
|
||||||
|
return nil, fmt.Errorf("dkim: no domain specified")
|
||||||
|
}
|
||||||
|
if options.Selector == "" {
|
||||||
|
return nil, fmt.Errorf("dkim: no selector specified")
|
||||||
|
}
|
||||||
|
if options.Signer == nil {
|
||||||
|
return nil, fmt.Errorf("dkim: no signer specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
headerCan := options.HeaderCanonicalization
|
||||||
|
if headerCan == "" {
|
||||||
|
headerCan = CanonicalizationSimple
|
||||||
|
}
|
||||||
|
if _, ok := canonicalizers[headerCan]; !ok {
|
||||||
|
return nil, fmt.Errorf("dkim: unknown header canonicalization %q", headerCan)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyCan := options.BodyCanonicalization
|
||||||
|
if bodyCan == "" {
|
||||||
|
bodyCan = CanonicalizationSimple
|
||||||
|
}
|
||||||
|
if _, ok := canonicalizers[bodyCan]; !ok {
|
||||||
|
return nil, fmt.Errorf("dkim: unknown body canonicalization %q", bodyCan)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyAlgo string
|
||||||
|
switch options.Signer.Public().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
keyAlgo = "rsa"
|
||||||
|
case ed25519.PublicKey:
|
||||||
|
keyAlgo = "ed25519"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("dkim: unsupported key algorithm %T", options.Signer.Public())
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := options.Hash
|
||||||
|
var hashAlgo string
|
||||||
|
switch options.Hash {
|
||||||
|
case 0: // sha256 is the default
|
||||||
|
hash = crypto.SHA256
|
||||||
|
fallthrough
|
||||||
|
case crypto.SHA256:
|
||||||
|
hashAlgo = "sha256"
|
||||||
|
case crypto.SHA1:
|
||||||
|
return nil, fmt.Errorf("dkim: hash algorithm too weak: sha1")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("dkim: unsupported hash algorithm")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.HeaderKeys != nil {
|
||||||
|
ok := false
|
||||||
|
for _, k := range options.HeaderKeys {
|
||||||
|
if strings.EqualFold(k, "From") {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("dkim: the From header field must be signed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
|
||||||
|
s := &Signer{
|
||||||
|
pw: pw,
|
||||||
|
done: done,
|
||||||
|
}
|
||||||
|
|
||||||
|
closeReadWithError := func(err error) {
|
||||||
|
pr.CloseWithError(err)
|
||||||
|
done <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
// Read header
|
||||||
|
br := bufio.NewReader(pr)
|
||||||
|
h, err := readHeader(br)
|
||||||
|
if err != nil {
|
||||||
|
closeReadWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash body
|
||||||
|
hasher := hash.New()
|
||||||
|
can := canonicalizers[bodyCan].CanonicalizeBody(hasher)
|
||||||
|
if _, err := io.Copy(can, br); err != nil {
|
||||||
|
closeReadWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := can.Close(); err != nil {
|
||||||
|
closeReadWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bodyHashed := hasher.Sum(nil)
|
||||||
|
|
||||||
|
params := map[string]string{
|
||||||
|
"v": "1",
|
||||||
|
"a": keyAlgo + "-" + hashAlgo,
|
||||||
|
"bh": base64.StdEncoding.EncodeToString(bodyHashed),
|
||||||
|
"c": string(headerCan) + "/" + string(bodyCan),
|
||||||
|
"d": options.Domain,
|
||||||
|
//"l": "", // TODO
|
||||||
|
"s": options.Selector,
|
||||||
|
"t": formatTime(now()),
|
||||||
|
//"z": "", // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerKeys []string
|
||||||
|
if options.HeaderKeys != nil {
|
||||||
|
headerKeys = options.HeaderKeys
|
||||||
|
} else {
|
||||||
|
for _, kv := range h {
|
||||||
|
k, _ := parseHeaderField(kv)
|
||||||
|
headerKeys = append(headerKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params["h"] = formatTagList(headerKeys)
|
||||||
|
|
||||||
|
if options.Identifier != "" {
|
||||||
|
params["i"] = options.Identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.QueryMethods != nil {
|
||||||
|
methods := make([]string, len(options.QueryMethods))
|
||||||
|
for i, method := range options.QueryMethods {
|
||||||
|
methods[i] = string(method)
|
||||||
|
}
|
||||||
|
params["q"] = formatTagList(methods)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !options.Expiration.IsZero() {
|
||||||
|
params["x"] = formatTime(options.Expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash and sign headers
|
||||||
|
hasher.Reset()
|
||||||
|
picker := newHeaderPicker(h)
|
||||||
|
for _, k := range headerKeys {
|
||||||
|
kv := picker.Pick(k)
|
||||||
|
if kv == "" {
|
||||||
|
// The Signer MAY include more instances of a header field name
|
||||||
|
// in "h=" than there are actual corresponding header fields so
|
||||||
|
// that the signature will not verify if additional header
|
||||||
|
// fields of that name are added.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kv = canonicalizers[headerCan].CanonicalizeHeader(kv)
|
||||||
|
if _, err := io.WriteString(hasher, kv); err != nil {
|
||||||
|
closeReadWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params["b"] = ""
|
||||||
|
sigField := formatSignature(params)
|
||||||
|
sigField = canonicalizers[headerCan].CanonicalizeHeader(sigField)
|
||||||
|
sigField = strings.TrimRight(sigField, crlf)
|
||||||
|
if _, err := io.WriteString(hasher, sigField); err != nil {
|
||||||
|
closeReadWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hashed := hasher.Sum(nil)
|
||||||
|
|
||||||
|
// Don't pass Hash to Sign for ed25519 as it doesn't support it
|
||||||
|
// and will return an error ("ed25519: cannot sign hashed message").
|
||||||
|
if keyAlgo == "ed25519" {
|
||||||
|
hash = crypto.Hash(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := options.Signer.Sign(randReader, hashed, hash)
|
||||||
|
if err != nil {
|
||||||
|
closeReadWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params["b"] = base64.StdEncoding.EncodeToString(sig)
|
||||||
|
|
||||||
|
s.sigParams = params
|
||||||
|
closeReadWithError(nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.WriteCloser.
|
||||||
|
func (s *Signer) Write(b []byte) (n int, err error) {
|
||||||
|
return s.pw.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements io.WriteCloser. The error return by Close must be checked.
|
||||||
|
func (s *Signer) Close() error {
|
||||||
|
if err := s.pw.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return <-s.done
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature returns the whole DKIM-Signature header field. It can only be
|
||||||
|
// called after a successful Signer.Close call.
|
||||||
|
//
|
||||||
|
// The returned value contains both the header field name, its value and the
|
||||||
|
// final CRLF.
|
||||||
|
func (s *Signer) Signature() string {
|
||||||
|
if s.sigParams == nil {
|
||||||
|
panic("dkim: Signer.Signature must only be called after a succesful Signer.Close")
|
||||||
|
}
|
||||||
|
return formatSignature(s.sigParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs a message. It reads it from r and writes the signed version to w.
|
||||||
|
func Sign(w io.Writer, r io.Reader, options *SignOptions) error {
|
||||||
|
s, err := NewSigner(options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// We need to keep the message in a buffer so we can write the new DKIM
|
||||||
|
// header field before the rest of the message
|
||||||
|
var b bytes.Buffer
|
||||||
|
mw := io.MultiWriter(&b, s)
|
||||||
|
|
||||||
|
if _, err := io.Copy(mw, r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.WriteString(w, s.Signature()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(w, &b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSignature(params map[string]string) string {
|
||||||
|
sig := formatHeaderParams(headerFieldName, params)
|
||||||
|
return sig
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTagList(l []string) string {
|
||||||
|
return strings.Join(l, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTime(t time.Time) string {
|
||||||
|
return strconv.FormatInt(t.Unix(), 10)
|
||||||
|
}
|
462
vendor/github.com/emersion/go-msgauth/dkim/verify.go
generated
vendored
Normal file
462
vendor/github.com/emersion/go-msgauth/dkim/verify.go
generated
vendored
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
package dkim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type permFailError string
|
||||||
|
|
||||||
|
func (err permFailError) Error() string {
|
||||||
|
return "dkim: " + string(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPermFail returns true if the error returned by Verify is a permanent
|
||||||
|
// failure. A permanent failure is for instance a missing required field or a
|
||||||
|
// malformed header.
|
||||||
|
func IsPermFail(err error) bool {
|
||||||
|
_, ok := err.(permFailError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
type tempFailError string
|
||||||
|
|
||||||
|
func (err tempFailError) Error() string {
|
||||||
|
return "dkim: " + string(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTempFail returns true if the error returned by Verify is a temporary
|
||||||
|
// failure.
|
||||||
|
func IsTempFail(err error) bool {
|
||||||
|
_, ok := err.(tempFailError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
type failError string
|
||||||
|
|
||||||
|
func (err failError) Error() string {
|
||||||
|
return "dkim: " + string(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFail returns true if the error returned by Verify is a signature error.
|
||||||
|
func isFail(err error) bool {
|
||||||
|
_, ok := err.(failError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrTooManySignatures is returned by Verify when the message exceeds the
|
||||||
|
// maximum number of signatures.
|
||||||
|
var ErrTooManySignatures = errors.New("dkim: too many signatures")
|
||||||
|
|
||||||
|
var requiredTags = []string{"v", "a", "b", "bh", "d", "h", "s"}
|
||||||
|
|
||||||
|
// A Verification is produced by Verify when it checks if one signature is
|
||||||
|
// valid. If the signature is valid, Err is nil.
|
||||||
|
type Verification struct {
|
||||||
|
// The SDID claiming responsibility for an introduction of a message into the
|
||||||
|
// mail stream.
|
||||||
|
Domain string
|
||||||
|
// The Agent or User Identifier (AUID) on behalf of which the SDID is taking
|
||||||
|
// responsibility.
|
||||||
|
Identifier string
|
||||||
|
|
||||||
|
// The list of signed header fields.
|
||||||
|
HeaderKeys []string
|
||||||
|
|
||||||
|
// The time that this signature was created. If unknown, it's set to zero.
|
||||||
|
Time time.Time
|
||||||
|
// The expiration time. If the signature doesn't expire, it's set to zero.
|
||||||
|
Expiration time.Time
|
||||||
|
|
||||||
|
// Err is nil if the signature is valid.
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type signature struct {
|
||||||
|
i int
|
||||||
|
v string
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyOptions allows to customize the default signature verification
|
||||||
|
// behavior.
|
||||||
|
type VerifyOptions struct {
|
||||||
|
// LookupTXT returns the DNS TXT records for the given domain name. If nil,
|
||||||
|
// net.LookupTXT is used.
|
||||||
|
LookupTXT func(domain string) ([]string, error)
|
||||||
|
// MaxVerifications controls the maximum number of signature verifications
|
||||||
|
// to perform. If more signatures are present, the first MaxVerifications
|
||||||
|
// signatures are verified, the rest are ignored and ErrTooManySignatures
|
||||||
|
// is returned. If zero, there is no maximum.
|
||||||
|
MaxVerifications int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks if a message's signatures are valid. It returns one
|
||||||
|
// verification per signature.
|
||||||
|
//
|
||||||
|
// There is no guarantee that the reader will be completely consumed.
|
||||||
|
func Verify(r io.Reader) ([]*Verification, error) {
|
||||||
|
return VerifyWithOptions(r, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyWithOptions performs the same task as Verify, but allows specifying
|
||||||
|
// verification options.
|
||||||
|
func VerifyWithOptions(r io.Reader, options *VerifyOptions) ([]*Verification, error) {
|
||||||
|
// Read header
|
||||||
|
bufr := bufio.NewReader(r)
|
||||||
|
h, err := readHeader(bufr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan header fields for signatures
|
||||||
|
var signatures []*signature
|
||||||
|
for i, kv := range h {
|
||||||
|
k, v := parseHeaderField(kv)
|
||||||
|
if strings.EqualFold(k, headerFieldName) {
|
||||||
|
signatures = append(signatures, &signature{i, v})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tooManySignatures := false
|
||||||
|
if options != nil && options.MaxVerifications > 0 && len(signatures) > options.MaxVerifications {
|
||||||
|
tooManySignatures = true
|
||||||
|
signatures = signatures[:options.MaxVerifications]
|
||||||
|
}
|
||||||
|
|
||||||
|
var verifs []*Verification
|
||||||
|
if len(signatures) == 1 {
|
||||||
|
// If there is only one signature - just verify it.
|
||||||
|
v, err := verify(h, bufr, h[signatures[0].i], signatures[0].v, options)
|
||||||
|
if err != nil && !IsTempFail(err) && !IsPermFail(err) && !isFail(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v.Err = err
|
||||||
|
verifs = []*Verification{v}
|
||||||
|
} else {
|
||||||
|
verifs, err = parallelVerify(bufr, h, signatures, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tooManySignatures {
|
||||||
|
return verifs, ErrTooManySignatures
|
||||||
|
}
|
||||||
|
return verifs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parallelVerify(r io.Reader, h header, signatures []*signature, options *VerifyOptions) ([]*Verification, error) {
|
||||||
|
pipeWriters := make([]*io.PipeWriter, len(signatures))
|
||||||
|
// We can't pass pipeWriter to io.MultiWriter directly,
|
||||||
|
// we need a slice of io.Writer, but we also need *io.PipeWriter
|
||||||
|
// to call Close on it.
|
||||||
|
writers := make([]io.Writer, len(signatures))
|
||||||
|
chans := make([]chan *Verification, len(signatures))
|
||||||
|
|
||||||
|
for i, sig := range signatures {
|
||||||
|
// Be careful with loop variables and goroutines.
|
||||||
|
i, sig := i, sig
|
||||||
|
|
||||||
|
chans[i] = make(chan *Verification, 1)
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
writers[i] = pw
|
||||||
|
pipeWriters[i] = pw
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
v, err := verify(h, pr, h[sig.i], sig.v, options)
|
||||||
|
|
||||||
|
// Make sure we consume the whole reader, otherwise io.Copy on
|
||||||
|
// other side can block forever.
|
||||||
|
io.Copy(ioutil.Discard, pr)
|
||||||
|
|
||||||
|
v.Err = err
|
||||||
|
chans[i] <- v
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(io.MultiWriter(writers...), r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, wr := range pipeWriters {
|
||||||
|
wr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
verifications := make([]*Verification, len(signatures))
|
||||||
|
for i, ch := range chans {
|
||||||
|
verifications[i] = <-ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return unexpected failures as a separate error.
|
||||||
|
for _, v := range verifications {
|
||||||
|
err := v.Err
|
||||||
|
if err != nil && !IsTempFail(err) && !IsPermFail(err) && !isFail(err) {
|
||||||
|
v.Err = nil
|
||||||
|
return verifications, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verifications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verify(h header, r io.Reader, sigField, sigValue string, options *VerifyOptions) (*Verification, error) {
|
||||||
|
verif := new(Verification)
|
||||||
|
|
||||||
|
params, err := parseHeaderParams(sigValue)
|
||||||
|
if err != nil {
|
||||||
|
return verif, permFailError("malformed signature tags: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if params["v"] != "1" {
|
||||||
|
return verif, permFailError("incompatible signature version")
|
||||||
|
}
|
||||||
|
|
||||||
|
verif.Domain = stripWhitespace(params["d"])
|
||||||
|
|
||||||
|
for _, tag := range requiredTags {
|
||||||
|
if _, ok := params[tag]; !ok {
|
||||||
|
return verif, permFailError("signature missing required tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i, ok := params["i"]; ok {
|
||||||
|
verif.Identifier = stripWhitespace(i)
|
||||||
|
if !strings.HasSuffix(verif.Identifier, "@"+verif.Domain) && !strings.HasSuffix(verif.Identifier, "."+verif.Domain) {
|
||||||
|
return verif, permFailError("domain mismatch")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
verif.Identifier = "@" + verif.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
headerKeys := parseTagList(params["h"])
|
||||||
|
ok := false
|
||||||
|
for _, k := range headerKeys {
|
||||||
|
if strings.EqualFold(k, "from") {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return verif, permFailError("From field not signed")
|
||||||
|
}
|
||||||
|
verif.HeaderKeys = headerKeys
|
||||||
|
|
||||||
|
if timeStr, ok := params["t"]; ok {
|
||||||
|
t, err := parseTime(timeStr)
|
||||||
|
if err != nil {
|
||||||
|
return verif, permFailError("malformed time: " + err.Error())
|
||||||
|
}
|
||||||
|
verif.Time = t
|
||||||
|
}
|
||||||
|
if expiresStr, ok := params["x"]; ok {
|
||||||
|
t, err := parseTime(expiresStr)
|
||||||
|
if err != nil {
|
||||||
|
return verif, permFailError("malformed expiration time: " + err.Error())
|
||||||
|
}
|
||||||
|
verif.Expiration = t
|
||||||
|
if now().After(t) {
|
||||||
|
return verif, permFailError("signature has expired")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query public key
|
||||||
|
// TODO: compute hash in parallel
|
||||||
|
methods := []string{string(QueryMethodDNSTXT)}
|
||||||
|
if methodsStr, ok := params["q"]; ok {
|
||||||
|
methods = parseTagList(methodsStr)
|
||||||
|
}
|
||||||
|
var res *queryResult
|
||||||
|
for _, method := range methods {
|
||||||
|
if query, ok := queryMethods[QueryMethod(method)]; ok {
|
||||||
|
if options != nil {
|
||||||
|
res, err = query(verif.Domain, stripWhitespace(params["s"]), options.LookupTXT)
|
||||||
|
} else {
|
||||||
|
res, err = query(verif.Domain, stripWhitespace(params["s"]), nil)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return verif, err
|
||||||
|
} else if res == nil {
|
||||||
|
return verif, permFailError("unsupported public key query method")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse algos
|
||||||
|
keyAlgo, hashAlgo, ok := strings.Cut(stripWhitespace(params["a"]), "-")
|
||||||
|
if !ok {
|
||||||
|
return verif, permFailError("malformed algorithm name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check hash algo
|
||||||
|
if res.HashAlgos != nil {
|
||||||
|
ok := false
|
||||||
|
for _, algo := range res.HashAlgos {
|
||||||
|
if algo == hashAlgo {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return verif, permFailError("inappropriate hash algorithm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var hash crypto.Hash
|
||||||
|
switch hashAlgo {
|
||||||
|
case "sha1":
|
||||||
|
// RFC 8301 section 3.1: rsa-sha1 MUST NOT be used for signing or
|
||||||
|
// verifying.
|
||||||
|
return verif, permFailError(fmt.Sprintf("hash algorithm too weak: %v", hashAlgo))
|
||||||
|
case "sha256":
|
||||||
|
hash = crypto.SHA256
|
||||||
|
default:
|
||||||
|
return verif, permFailError("unsupported hash algorithm")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check key algo
|
||||||
|
if res.KeyAlgo != keyAlgo {
|
||||||
|
return verif, permFailError("inappropriate key algorithm")
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Services != nil {
|
||||||
|
ok := false
|
||||||
|
for _, s := range res.Services {
|
||||||
|
if s == "email" {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return verif, permFailError("inappropriate service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headerCan, bodyCan := parseCanonicalization(params["c"])
|
||||||
|
if _, ok := canonicalizers[headerCan]; !ok {
|
||||||
|
return verif, permFailError("unsupported header canonicalization algorithm")
|
||||||
|
}
|
||||||
|
if _, ok := canonicalizers[bodyCan]; !ok {
|
||||||
|
return verif, permFailError("unsupported body canonicalization algorithm")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The body length "l" parameter is insecure, because it allows parts of
|
||||||
|
// the message body to not be signed. Reject messages which have it set.
|
||||||
|
if _, ok := params["l"]; ok {
|
||||||
|
// TODO: technically should be policyError
|
||||||
|
return verif, failError("message contains an insecure body length tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse body hash and signature
|
||||||
|
bodyHashed, err := decodeBase64String(params["bh"])
|
||||||
|
if err != nil {
|
||||||
|
return verif, permFailError("malformed body hash: " + err.Error())
|
||||||
|
}
|
||||||
|
sig, err := decodeBase64String(params["b"])
|
||||||
|
if err != nil {
|
||||||
|
return verif, permFailError("malformed signature: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check body hash
|
||||||
|
hasher := hash.New()
|
||||||
|
wc := canonicalizers[bodyCan].CanonicalizeBody(hasher)
|
||||||
|
if _, err := io.Copy(wc, r); err != nil {
|
||||||
|
return verif, err
|
||||||
|
}
|
||||||
|
if err := wc.Close(); err != nil {
|
||||||
|
return verif, err
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeCompare(hasher.Sum(nil), bodyHashed) != 1 {
|
||||||
|
return verif, failError("body hash did not verify")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute data hash
|
||||||
|
hasher.Reset()
|
||||||
|
picker := newHeaderPicker(h)
|
||||||
|
for _, key := range headerKeys {
|
||||||
|
kv := picker.Pick(key)
|
||||||
|
if kv == "" {
|
||||||
|
// The field MAY contain names of header fields that do not exist
|
||||||
|
// when signed; nonexistent header fields do not contribute to the
|
||||||
|
// signature computation
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kv = canonicalizers[headerCan].CanonicalizeHeader(kv)
|
||||||
|
if _, err := hasher.Write([]byte(kv)); err != nil {
|
||||||
|
return verif, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canSigField := removeSignature(sigField)
|
||||||
|
canSigField = canonicalizers[headerCan].CanonicalizeHeader(canSigField)
|
||||||
|
canSigField = strings.TrimRight(canSigField, "\r\n")
|
||||||
|
if _, err := hasher.Write([]byte(canSigField)); err != nil {
|
||||||
|
return verif, err
|
||||||
|
}
|
||||||
|
hashed := hasher.Sum(nil)
|
||||||
|
|
||||||
|
// Check signature
|
||||||
|
if err := res.Verifier.Verify(hash, hashed, sig); err != nil {
|
||||||
|
return verif, failError("signature did not verify: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return verif, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTagList(s string) []string {
|
||||||
|
tags := strings.Split(s, ":")
|
||||||
|
for i, t := range tags {
|
||||||
|
tags[i] = stripWhitespace(t)
|
||||||
|
}
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCanonicalization(s string) (headerCan, bodyCan Canonicalization) {
|
||||||
|
headerCan = CanonicalizationSimple
|
||||||
|
bodyCan = CanonicalizationSimple
|
||||||
|
|
||||||
|
cans := strings.SplitN(stripWhitespace(s), "/", 2)
|
||||||
|
if cans[0] != "" {
|
||||||
|
headerCan = Canonicalization(cans[0])
|
||||||
|
}
|
||||||
|
if len(cans) > 1 {
|
||||||
|
bodyCan = Canonicalization(cans[1])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTime(s string) (time.Time, error) {
|
||||||
|
sec, err := strconv.ParseInt(stripWhitespace(s), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return time.Unix(sec, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBase64String(s string) ([]byte, error) {
|
||||||
|
return base64.StdEncoding.DecodeString(stripWhitespace(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripWhitespace(s string) string {
|
||||||
|
return strings.Map(func(r rune) rune {
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sigRegex = regexp.MustCompile(`(b\s*=)[^;]+`)
|
||||||
|
|
||||||
|
func removeSignature(s string) string {
|
||||||
|
return sigRegex.ReplaceAllString(s, "$1")
|
||||||
|
}
|
24
vendor/github.com/toorop/go-dkim/.gitignore
generated
vendored
24
vendor/github.com/toorop/go-dkim/.gitignore
generated
vendored
@ -1,24 +0,0 @@
|
|||||||
# 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
|
|
56
vendor/github.com/toorop/go-dkim/README.md
generated
vendored
56
vendor/github.com/toorop/go-dkim/README.md
generated
vendored
@ -1,56 +0,0 @@
|
|||||||
# go-dkim
|
|
||||||
DKIM package for Golang
|
|
||||||
|
|
||||||
[](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)
|
|
564
vendor/github.com/toorop/go-dkim/dkim.go
generated
vendored
564
vendor/github.com/toorop/go-dkim/dkim.go
generated
vendored
@ -1,564 +0,0 @@
|
|||||||
// 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
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// PrivateKey
|
|
||||||
if len(options.PrivateKey) == 0 {
|
|
||||||
return ErrSignPrivateKeyRequired
|
|
||||||
}
|
|
||||||
d, _ := pem.Decode(options.PrivateKey)
|
|
||||||
if d == nil {
|
|
||||||
return ErrCandNotParsePrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to parse it as PKCS1 otherwise try PKCS8
|
|
||||||
if key, err := x509.ParsePKCS1PrivateKey(d.Bytes); err != nil {
|
|
||||||
if key, err := x509.ParsePKCS8PrivateKey(d.Bytes); err != nil {
|
|
||||||
return ErrCandNotParsePrivateKey
|
|
||||||
} else {
|
|
||||||
privateKey = key.(*rsa.PrivateKey)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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 := GetHeader(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
545
vendor/github.com/toorop/go-dkim/dkimHeader.go
generated
vendored
@ -1,545 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeader 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 GetHeader(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
94
vendor/github.com/toorop/go-dkim/errors.go
generated
vendored
@ -1,94 +0,0 @@
|
|||||||
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
181
vendor/github.com/toorop/go-dkim/pubKeyRep.go
generated
vendored
@ -1,181 +0,0 @@
|
|||||||
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
4
vendor/github.com/toorop/go-dkim/watch
generated
vendored
@ -1,4 +0,0 @@
|
|||||||
while true
|
|
||||||
do
|
|
||||||
inotifywait -q -r -e modify,attrib,close_write,move,create,delete . && echo "--------------" && go test -v
|
|
||||||
done
|
|
69
vendor/golang.org/x/crypto/ed25519/ed25519.go
generated
vendored
Normal file
69
vendor/golang.org/x/crypto/ed25519/ed25519.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package ed25519 implements the Ed25519 signature algorithm. See
|
||||||
|
// https://ed25519.cr.yp.to/.
|
||||||
|
//
|
||||||
|
// These functions are also compatible with the “Ed25519” function defined in
|
||||||
|
// RFC 8032. However, unlike RFC 8032's formulation, this package's private key
|
||||||
|
// representation includes a public key suffix to make multiple signing
|
||||||
|
// operations with the same key more efficient. This package refers to the RFC
|
||||||
|
// 8032 private key as the “seed”.
|
||||||
|
//
|
||||||
|
// This package is a wrapper around the standard library crypto/ed25519 package.
|
||||||
|
package ed25519
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PublicKeySize is the size, in bytes, of public keys as used in this package.
|
||||||
|
PublicKeySize = 32
|
||||||
|
// PrivateKeySize is the size, in bytes, of private keys as used in this package.
|
||||||
|
PrivateKeySize = 64
|
||||||
|
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
|
||||||
|
SignatureSize = 64
|
||||||
|
// SeedSize is the size, in bytes, of private key seeds. These are the private key representations used by RFC 8032.
|
||||||
|
SeedSize = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// PublicKey is the type of Ed25519 public keys.
|
||||||
|
//
|
||||||
|
// This type is an alias for crypto/ed25519's PublicKey type.
|
||||||
|
// See the crypto/ed25519 package for the methods on this type.
|
||||||
|
type PublicKey = ed25519.PublicKey
|
||||||
|
|
||||||
|
// PrivateKey is the type of Ed25519 private keys. It implements crypto.Signer.
|
||||||
|
//
|
||||||
|
// This type is an alias for crypto/ed25519's PrivateKey type.
|
||||||
|
// See the crypto/ed25519 package for the methods on this type.
|
||||||
|
type PrivateKey = ed25519.PrivateKey
|
||||||
|
|
||||||
|
// GenerateKey generates a public/private key pair using entropy from rand.
|
||||||
|
// If rand is nil, crypto/rand.Reader will be used.
|
||||||
|
func GenerateKey(rand io.Reader) (PublicKey, PrivateKey, error) {
|
||||||
|
return ed25519.GenerateKey(rand)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyFromSeed calculates a private key from a seed. It will panic if
|
||||||
|
// len(seed) is not SeedSize. This function is provided for interoperability
|
||||||
|
// with RFC 8032. RFC 8032's private keys correspond to seeds in this
|
||||||
|
// package.
|
||||||
|
func NewKeyFromSeed(seed []byte) PrivateKey {
|
||||||
|
return ed25519.NewKeyFromSeed(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs the message with privateKey and returns a signature. It will
|
||||||
|
// panic if len(privateKey) is not PrivateKeySize.
|
||||||
|
func Sign(privateKey PrivateKey, message []byte) []byte {
|
||||||
|
return ed25519.Sign(privateKey, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify reports whether sig is a valid signature of message by publicKey. It
|
||||||
|
// will panic if len(publicKey) is not PublicKeySize.
|
||||||
|
func Verify(publicKey PublicKey, message, sig []byte) bool {
|
||||||
|
return ed25519.Verify(publicKey, message, sig)
|
||||||
|
}
|
5
vendor/modules.txt
vendored
5
vendor/modules.txt
vendored
@ -10,6 +10,9 @@ github.com/GehirnInc/crypt/md5_crypt
|
|||||||
# github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
# github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
## explicit
|
## explicit
|
||||||
github.com/docopt/docopt-go
|
github.com/docopt/docopt-go
|
||||||
|
# github.com/emersion/go-msgauth v0.6.8
|
||||||
|
## explicit; go 1.18
|
||||||
|
github.com/emersion/go-msgauth/dkim
|
||||||
# github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
|
# github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
|
||||||
## explicit
|
## explicit
|
||||||
github.com/ergochat/confusables
|
github.com/ergochat/confusables
|
||||||
@ -75,7 +78,6 @@ github.com/tidwall/rtred/base
|
|||||||
github.com/tidwall/tinyqueue
|
github.com/tidwall/tinyqueue
|
||||||
# github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208
|
# github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208
|
||||||
## explicit
|
## explicit
|
||||||
github.com/toorop/go-dkim
|
|
||||||
# github.com/xdg-go/pbkdf2 v1.0.0
|
# github.com/xdg-go/pbkdf2 v1.0.0
|
||||||
## explicit; go 1.9
|
## explicit; go 1.9
|
||||||
github.com/xdg-go/pbkdf2
|
github.com/xdg-go/pbkdf2
|
||||||
@ -86,6 +88,7 @@ github.com/xdg-go/scram
|
|||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
golang.org/x/crypto/bcrypt
|
golang.org/x/crypto/bcrypt
|
||||||
golang.org/x/crypto/blowfish
|
golang.org/x/crypto/blowfish
|
||||||
|
golang.org/x/crypto/ed25519
|
||||||
golang.org/x/crypto/hkdf
|
golang.org/x/crypto/hkdf
|
||||||
golang.org/x/crypto/pbkdf2
|
golang.org/x/crypto/pbkdf2
|
||||||
# golang.org/x/sys v0.29.0
|
# golang.org/x/sys v0.29.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user