From 68cee9e2cd08ffddb8e04f87257d69ff910f1f5c Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 7 Apr 2025 00:24:08 -0400 Subject: [PATCH] use emersion/go-msgauth for DKIM (#2242) Fixes #1041 (support ed25519-sha256 for DKIM) --- go.mod | 3 +- go.sum | 6 +- irc/email/dkim.go | 98 ++- irc/email/email.go | 2 +- .../go-dkim => emersion/go-msgauth}/LICENSE | 5 +- .../emersion/go-msgauth/dkim/canonical.go | 199 ++++++ .../emersion/go-msgauth/dkim/dkim.go | 23 + .../emersion/go-msgauth/dkim/header.go | 167 ++++++ .../emersion/go-msgauth/dkim/query.go | 184 ++++++ .../emersion/go-msgauth/dkim/sign.go | 346 +++++++++++ .../emersion/go-msgauth/dkim/verify.go | 462 ++++++++++++++ vendor/github.com/toorop/go-dkim/.gitignore | 24 - vendor/github.com/toorop/go-dkim/README.md | 56 -- vendor/github.com/toorop/go-dkim/dkim.go | 564 ------------------ .../github.com/toorop/go-dkim/dkimHeader.go | 545 ----------------- vendor/github.com/toorop/go-dkim/errors.go | 94 --- vendor/github.com/toorop/go-dkim/pubKeyRep.go | 181 ------ vendor/github.com/toorop/go-dkim/watch | 4 - vendor/golang.org/x/crypto/ed25519/ed25519.go | 69 +++ vendor/modules.txt | 5 +- 20 files changed, 1533 insertions(+), 1504 deletions(-) rename vendor/github.com/{toorop/go-dkim => emersion/go-msgauth}/LICENSE (94%) create mode 100644 vendor/github.com/emersion/go-msgauth/dkim/canonical.go create mode 100644 vendor/github.com/emersion/go-msgauth/dkim/dkim.go create mode 100644 vendor/github.com/emersion/go-msgauth/dkim/header.go create mode 100644 vendor/github.com/emersion/go-msgauth/dkim/query.go create mode 100644 vendor/github.com/emersion/go-msgauth/dkim/sign.go create mode 100644 vendor/github.com/emersion/go-msgauth/dkim/verify.go delete mode 100644 vendor/github.com/toorop/go-dkim/.gitignore delete mode 100644 vendor/github.com/toorop/go-dkim/README.md delete mode 100644 vendor/github.com/toorop/go-dkim/dkim.go delete mode 100644 vendor/github.com/toorop/go-dkim/dkimHeader.go delete mode 100644 vendor/github.com/toorop/go-dkim/errors.go delete mode 100644 vendor/github.com/toorop/go-dkim/pubKeyRep.go delete mode 100644 vendor/github.com/toorop/go-dkim/watch create mode 100644 vendor/golang.org/x/crypto/ed25519/ed25519.go diff --git a/go.mod b/go.mod index 7eacdf15..6b3daa4a 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 github.com/ergochat/irc-go v0.5.0-rc2 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/gorilla/websocket v1.4.2 github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd @@ -18,7 +17,6 @@ require ( github.com/onsi/gomega v1.9.0 // indirect github.com/stretchr/testify v1.4.0 // indirect 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 golang.org/x/crypto v0.32.0 golang.org/x/term v0.28.0 @@ -27,6 +25,7 @@ require ( ) require ( + github.com/emersion/go-msgauth v0.6.8 github.com/ergochat/webpush-go/v2 v2.0.0 github.com/golang-jwt/jwt/v5 v5.2.2 ) diff --git a/go.sum b/go.sum index 23108e37..9cc360fc 100644 --- a/go.sum +++ b/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/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/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/go.mod h1:mov+uh1DPWsltdQnOdzn08UO9GsJ3MEvhtu0Ci37fdk= 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/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-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/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 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/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= 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/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= diff --git a/irc/email/dkim.go b/irc/email/dkim.go index 10952478..b4324d2c 100644 --- a/irc/email/dkim.go +++ b/irc/email/dkim.go @@ -4,9 +4,18 @@ package email import ( + "bytes" + "crypto" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "errors" - dkim "github.com/toorop/go-dkim" + "fmt" + "os" + + dkim "github.com/emersion/go-msgauth/dkim" ) var ( @@ -17,38 +26,77 @@ type DKIMConfig struct { Domain string Selector string KeyFile string `yaml:"key-file"` - keyBytes []byte + privKey crypto.Signer +} + +func (dkim *DKIMConfig) Enabled() bool { + return dkim.Domain != "" } func (dkim *DKIMConfig) Postprocess() (err error) { - if dkim.Domain != "" { - if dkim.Selector == "" || dkim.KeyFile == "" { - return ErrMissingFields - } - dkim.keyBytes, err = os.ReadFile(dkim.KeyFile) - if err != nil { - return err - } + if !dkim.Enabled() { + return nil } + + 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 } -var defaultOptions = dkim.SigOptions{ - Version: 1, - Canonicalization: "relaxed/relaxed", - Algo: "rsa-sha256", - Headers: []string{"from", "to", "subject", "message-id", "date"}, - BodyLength: 0, - QueryMethods: []string{"dns/txt"}, - AddSignatureTimestamp: true, - SignatureExpireIn: 0, +func parseDKIMPrivKey(input []byte) (crypto.Signer, error) { + if len(input) == 0 { + return nil, errors.New("DKIM private key is empty") + } + + // raw ed25519 private key format + if len(input) == ed25519.PrivateKeySize { + return ed25519.PrivateKey(input), nil + } + + 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) { - options := defaultOptions - options.PrivateKey = dkimConfig.keyBytes - options.Domain = dkimConfig.Domain - options.Selector = dkimConfig.Selector - err = dkim.Sign(&message, options) - return message, err + options := dkim.SignOptions{ + Domain: dkimConfig.Domain, + Selector: dkimConfig.Selector, + Signer: dkimConfig.privKey, + HeaderCanonicalization: dkim.CanonicalizationRelaxed, + 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 } diff --git a/irc/email/email.go b/irc/email/email.go index 1d6bd1ce..a23f9440 100644 --- a/irc/email/email.go +++ b/irc/email/email.go @@ -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) if err != nil { return diff --git a/vendor/github.com/toorop/go-dkim/LICENSE b/vendor/github.com/emersion/go-msgauth/LICENSE similarity index 94% rename from vendor/github.com/toorop/go-dkim/LICENSE rename to vendor/github.com/emersion/go-msgauth/LICENSE index f1afb74f..60fef8a4 100644 --- a/vendor/github.com/toorop/go-dkim/LICENSE +++ b/vendor/github.com/emersion/go-msgauth/LICENSE @@ -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 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, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/vendor/github.com/emersion/go-msgauth/dkim/canonical.go b/vendor/github.com/emersion/go-msgauth/dkim/canonical.go new file mode 100644 index 00000000..64ffd0a9 --- /dev/null +++ b/vendor/github.com/emersion/go-msgauth/dkim/canonical.go @@ -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 +} diff --git a/vendor/github.com/emersion/go-msgauth/dkim/dkim.go b/vendor/github.com/emersion/go-msgauth/dkim/dkim.go new file mode 100644 index 00000000..d9d51cc6 --- /dev/null +++ b/vendor/github.com/emersion/go-msgauth/dkim/dkim.go @@ -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" diff --git a/vendor/github.com/emersion/go-msgauth/dkim/header.go b/vendor/github.com/emersion/go-msgauth/dkim/header.go new file mode 100644 index 00000000..dddd81f7 --- /dev/null +++ b/vendor/github.com/emersion/go-msgauth/dkim/header.go @@ -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 "" +} diff --git a/vendor/github.com/emersion/go-msgauth/dkim/query.go b/vendor/github.com/emersion/go-msgauth/dkim/query.go new file mode 100644 index 00000000..33f8556e --- /dev/null +++ b/vendor/github.com/emersion/go-msgauth/dkim/query.go @@ -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 +} diff --git a/vendor/github.com/emersion/go-msgauth/dkim/sign.go b/vendor/github.com/emersion/go-msgauth/dkim/sign.go new file mode 100644 index 00000000..c4ba8c3c --- /dev/null +++ b/vendor/github.com/emersion/go-msgauth/dkim/sign.go @@ -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) +} diff --git a/vendor/github.com/emersion/go-msgauth/dkim/verify.go b/vendor/github.com/emersion/go-msgauth/dkim/verify.go new file mode 100644 index 00000000..ce942644 --- /dev/null +++ b/vendor/github.com/emersion/go-msgauth/dkim/verify.go @@ -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") +} diff --git a/vendor/github.com/toorop/go-dkim/.gitignore b/vendor/github.com/toorop/go-dkim/.gitignore deleted file mode 100644 index daf913b1..00000000 --- a/vendor/github.com/toorop/go-dkim/.gitignore +++ /dev/null @@ -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 diff --git a/vendor/github.com/toorop/go-dkim/README.md b/vendor/github.com/toorop/go-dkim/README.md deleted file mode 100644 index 49567395..00000000 --- a/vendor/github.com/toorop/go-dkim/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# go-dkim -DKIM package for Golang - -[![GoDoc](https://godoc.org/github.com/toorop/go-dkim?status.svg)](https://godoc.org/github.com/toorop/go-dkim) - -## Getting started - -### Install -``` - go get github.com/toorop/go-dkim -``` -Warning: you need to use Go 1.4.2-master or 1.4.3 (when it will be available) -see https://github.com/golang/go/issues/10482 fro more info. - -### Sign email - -```go -import ( - dkim "github.com/toorop/go-dkim" -) - -func main(){ - // email is the email to sign (byte slice) - // privateKey the private key (pem encoded, byte slice ) - options := dkim.NewSigOptions() - options.PrivateKey = privateKey - options.Domain = "mydomain.tld" - options.Selector = "myselector" - options.SignatureExpireIn = 3600 - options.BodyLength = 50 - options.Headers = []string{"from", "date", "mime-version", "received", "received"} - options.AddSignatureTimestamp = true - options.Canonicalization = "relaxed/relaxed" - err := dkim.Sign(&email, options) - // handle err.. - - // And... that's it, 'email' is signed ! Amazing© !!! -} -``` - -### Verify -```go -import ( - dkim "github.com/toorop/go-dkim" -) - -func main(){ - // email is the email to verify (byte slice) - status, err := Verify(&email) - // handle status, err (see godoc for status) -} -``` - -## Todo - -- [ ] handle z tag (copied header fields used for diagnostic use) diff --git a/vendor/github.com/toorop/go-dkim/dkim.go b/vendor/github.com/toorop/go-dkim/dkim.go deleted file mode 100644 index 3ed5c88f..00000000 --- a/vendor/github.com/toorop/go-dkim/dkim.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/toorop/go-dkim/dkimHeader.go b/vendor/github.com/toorop/go-dkim/dkimHeader.go deleted file mode 100644 index 14a289ee..00000000 --- a/vendor/github.com/toorop/go-dkim/dkimHeader.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/toorop/go-dkim/errors.go b/vendor/github.com/toorop/go-dkim/errors.go deleted file mode 100644 index 80a99da3..00000000 --- a/vendor/github.com/toorop/go-dkim/errors.go +++ /dev/null @@ -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") -) diff --git a/vendor/github.com/toorop/go-dkim/pubKeyRep.go b/vendor/github.com/toorop/go-dkim/pubKeyRep.go deleted file mode 100644 index 7a3ecf6f..00000000 --- a/vendor/github.com/toorop/go-dkim/pubKeyRep.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/toorop/go-dkim/watch b/vendor/github.com/toorop/go-dkim/watch deleted file mode 100644 index 82b58445..00000000 --- a/vendor/github.com/toorop/go-dkim/watch +++ /dev/null @@ -1,4 +0,0 @@ -while true -do -inotifywait -q -r -e modify,attrib,close_write,move,create,delete . && echo "--------------" && go test -v -done \ No newline at end of file diff --git a/vendor/golang.org/x/crypto/ed25519/ed25519.go b/vendor/golang.org/x/crypto/ed25519/ed25519.go new file mode 100644 index 00000000..59b3a95a --- /dev/null +++ b/vendor/golang.org/x/crypto/ed25519/ed25519.go @@ -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) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 849452da..291ca839 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -10,6 +10,9 @@ github.com/GehirnInc/crypt/md5_crypt # github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 ## explicit 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 ## explicit github.com/ergochat/confusables @@ -75,7 +78,6 @@ github.com/tidwall/rtred/base github.com/tidwall/tinyqueue # github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 ## explicit -github.com/toorop/go-dkim # github.com/xdg-go/pbkdf2 v1.0.0 ## explicit; go 1.9 github.com/xdg-go/pbkdf2 @@ -86,6 +88,7 @@ github.com/xdg-go/scram ## explicit; go 1.20 golang.org/x/crypto/bcrypt golang.org/x/crypto/blowfish +golang.org/x/crypto/ed25519 golang.org/x/crypto/hkdf golang.org/x/crypto/pbkdf2 # golang.org/x/sys v0.29.0