mirror of
https://github.com/ergochat/ergo.git
synced 2025-04-19 22:37:53 +02:00
use emersion/go-msgauth for DKIM (#2242)
Fixes #1041 (support ed25519-sha256 for DKIM)
This commit is contained in:
parent
9c3173f573
commit
68cee9e2cd
3
go.mod
3
go.mod
@ -10,7 +10,6 @@ require (
|
||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
|
||||
github.com/ergochat/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
|
||||
)
|
||||
|
6
go.sum
6
go.sum
@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
199
vendor/github.com/emersion/go-msgauth/dkim/canonical.go
generated
vendored
Normal file
199
vendor/github.com/emersion/go-msgauth/dkim/canonical.go
generated
vendored
Normal file
@ -0,0 +1,199 @@
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Canonicalization is a canonicalization algorithm.
|
||||
type Canonicalization string
|
||||
|
||||
const (
|
||||
CanonicalizationSimple Canonicalization = "simple"
|
||||
CanonicalizationRelaxed = "relaxed"
|
||||
)
|
||||
|
||||
type canonicalizer interface {
|
||||
CanonicalizeHeader(s string) string
|
||||
CanonicalizeBody(w io.Writer) io.WriteCloser
|
||||
}
|
||||
|
||||
var canonicalizers = map[Canonicalization]canonicalizer{
|
||||
CanonicalizationSimple: new(simpleCanonicalizer),
|
||||
CanonicalizationRelaxed: new(relaxedCanonicalizer),
|
||||
}
|
||||
|
||||
// crlfFixer fixes any lone LF without a preceding CR.
|
||||
type crlfFixer struct {
|
||||
cr bool
|
||||
}
|
||||
|
||||
func (cf *crlfFixer) Fix(b []byte) []byte {
|
||||
res := make([]byte, 0, len(b))
|
||||
for _, ch := range b {
|
||||
prevCR := cf.cr
|
||||
cf.cr = false
|
||||
switch ch {
|
||||
case '\r':
|
||||
cf.cr = true
|
||||
case '\n':
|
||||
if !prevCR {
|
||||
res = append(res, '\r')
|
||||
}
|
||||
}
|
||||
res = append(res, ch)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type simpleCanonicalizer struct{}
|
||||
|
||||
func (c *simpleCanonicalizer) CanonicalizeHeader(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
type simpleBodyCanonicalizer struct {
|
||||
w io.Writer
|
||||
crlfBuf []byte
|
||||
crlfFixer crlfFixer
|
||||
}
|
||||
|
||||
func (c *simpleBodyCanonicalizer) Write(b []byte) (int, error) {
|
||||
written := len(b)
|
||||
b = append(c.crlfBuf, b...)
|
||||
|
||||
b = c.crlfFixer.Fix(b)
|
||||
|
||||
end := len(b)
|
||||
// If it ends with \r, maybe the next write will begin with \n
|
||||
if end > 0 && b[end-1] == '\r' {
|
||||
end--
|
||||
}
|
||||
// Keep all \r\n sequences
|
||||
for end >= 2 {
|
||||
prev := b[end-2]
|
||||
cur := b[end-1]
|
||||
if prev != '\r' || cur != '\n' {
|
||||
break
|
||||
}
|
||||
end -= 2
|
||||
}
|
||||
|
||||
c.crlfBuf = b[end:]
|
||||
|
||||
var err error
|
||||
if end > 0 {
|
||||
_, err = c.w.Write(b[:end])
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
||||
func (c *simpleBodyCanonicalizer) Close() error {
|
||||
// Flush crlfBuf if it ends with a single \r (without a matching \n)
|
||||
if len(c.crlfBuf) > 0 && c.crlfBuf[len(c.crlfBuf)-1] == '\r' {
|
||||
if _, err := c.w.Write(c.crlfBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.crlfBuf = nil
|
||||
|
||||
if _, err := c.w.Write([]byte(crlf)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *simpleCanonicalizer) CanonicalizeBody(w io.Writer) io.WriteCloser {
|
||||
return &simpleBodyCanonicalizer{w: w}
|
||||
}
|
||||
|
||||
type relaxedCanonicalizer struct{}
|
||||
|
||||
func (c *relaxedCanonicalizer) CanonicalizeHeader(s string) string {
|
||||
k, v, ok := strings.Cut(s, ":")
|
||||
if !ok {
|
||||
return strings.TrimSpace(strings.ToLower(s)) + ":" + crlf
|
||||
}
|
||||
|
||||
k = strings.TrimSpace(strings.ToLower(k))
|
||||
v = strings.Join(strings.FieldsFunc(v, func(r rune) bool {
|
||||
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
}), " ")
|
||||
return k + ":" + v + crlf
|
||||
}
|
||||
|
||||
type relaxedBodyCanonicalizer struct {
|
||||
w io.Writer
|
||||
crlfBuf []byte
|
||||
wsp bool
|
||||
written bool
|
||||
crlfFixer crlfFixer
|
||||
}
|
||||
|
||||
func (c *relaxedBodyCanonicalizer) Write(b []byte) (int, error) {
|
||||
written := len(b)
|
||||
|
||||
b = c.crlfFixer.Fix(b)
|
||||
|
||||
canonical := make([]byte, 0, len(b))
|
||||
for _, ch := range b {
|
||||
if ch == ' ' || ch == '\t' {
|
||||
c.wsp = true
|
||||
} else if ch == '\r' || ch == '\n' {
|
||||
c.wsp = false
|
||||
c.crlfBuf = append(c.crlfBuf, ch)
|
||||
} else {
|
||||
if len(c.crlfBuf) > 0 {
|
||||
canonical = append(canonical, c.crlfBuf...)
|
||||
c.crlfBuf = c.crlfBuf[:0]
|
||||
}
|
||||
if c.wsp {
|
||||
canonical = append(canonical, ' ')
|
||||
c.wsp = false
|
||||
}
|
||||
|
||||
canonical = append(canonical, ch)
|
||||
}
|
||||
}
|
||||
|
||||
if !c.written && len(canonical) > 0 {
|
||||
c.written = true
|
||||
}
|
||||
|
||||
_, err := c.w.Write(canonical)
|
||||
return written, err
|
||||
}
|
||||
|
||||
func (c *relaxedBodyCanonicalizer) Close() error {
|
||||
if c.written {
|
||||
if _, err := c.w.Write([]byte(crlf)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *relaxedCanonicalizer) CanonicalizeBody(w io.Writer) io.WriteCloser {
|
||||
return &relaxedBodyCanonicalizer{w: w}
|
||||
}
|
||||
|
||||
type limitedWriter struct {
|
||||
W io.Writer
|
||||
N int64
|
||||
}
|
||||
|
||||
func (w *limitedWriter) Write(b []byte) (int, error) {
|
||||
if w.N <= 0 {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
skipped := 0
|
||||
if int64(len(b)) > w.N {
|
||||
b = b[:w.N]
|
||||
skipped = int(int64(len(b)) - w.N)
|
||||
}
|
||||
|
||||
n, err := w.W.Write(b)
|
||||
w.N -= int64(n)
|
||||
return n + skipped, err
|
||||
}
|
23
vendor/github.com/emersion/go-msgauth/dkim/dkim.go
generated
vendored
Normal file
23
vendor/github.com/emersion/go-msgauth/dkim/dkim.go
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
// Package dkim creates and verifies DKIM signatures, as specified in RFC 6376.
|
||||
//
|
||||
// # FAQ
|
||||
//
|
||||
// Why can't I verify a [net/mail.Message] directly? A [net/mail.Message]
|
||||
// header is already parsed, and whitespace characters (especially continuation
|
||||
// lines) are removed. Thus, the signature computed from the parsed header is
|
||||
// not the same as the one computed from the raw header.
|
||||
//
|
||||
// How can I publish my public key? You have to add a TXT record to your DNS
|
||||
// zone. See [RFC 6376 appendix C]. You can use the dkim-keygen tool included
|
||||
// in go-msgauth to generate the key and the TXT record.
|
||||
//
|
||||
// [RFC 6376 appendix C]: https://tools.ietf.org/html/rfc6376#appendix-C
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var now = time.Now
|
||||
|
||||
const headerFieldName = "DKIM-Signature"
|
167
vendor/github.com/emersion/go-msgauth/dkim/header.go
generated
vendored
Normal file
167
vendor/github.com/emersion/go-msgauth/dkim/header.go
generated
vendored
Normal file
@ -0,0 +1,167 @@
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/textproto"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const crlf = "\r\n"
|
||||
|
||||
type header []string
|
||||
|
||||
func readHeader(r *bufio.Reader) (header, error) {
|
||||
tr := textproto.NewReader(r)
|
||||
|
||||
var h header
|
||||
for {
|
||||
l, err := tr.ReadLine()
|
||||
if err != nil {
|
||||
return h, fmt.Errorf("failed to read header: %v", err)
|
||||
}
|
||||
|
||||
if len(l) == 0 {
|
||||
break
|
||||
} else if len(h) > 0 && (l[0] == ' ' || l[0] == '\t') {
|
||||
// This is a continuation line
|
||||
h[len(h)-1] += l + crlf
|
||||
} else {
|
||||
h = append(h, l+crlf)
|
||||
}
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func writeHeader(w io.Writer, h header) error {
|
||||
for _, kv := range h {
|
||||
if _, err := w.Write([]byte(kv)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := w.Write([]byte(crlf))
|
||||
return err
|
||||
}
|
||||
|
||||
func foldHeaderField(kv string) string {
|
||||
buf := bytes.NewBufferString(kv)
|
||||
|
||||
line := make([]byte, 75) // 78 - len("\r\n\s")
|
||||
first := true
|
||||
var fold strings.Builder
|
||||
for len, err := buf.Read(line); err != io.EOF; len, err = buf.Read(line) {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
fold.WriteString("\r\n ")
|
||||
}
|
||||
fold.Write(line[:len])
|
||||
}
|
||||
|
||||
return fold.String() + crlf
|
||||
}
|
||||
|
||||
func parseHeaderField(s string) (string, string) {
|
||||
key, value, _ := strings.Cut(s, ":")
|
||||
return strings.TrimSpace(key), strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func parseHeaderParams(s string) (map[string]string, error) {
|
||||
pairs := strings.Split(s, ";")
|
||||
params := make(map[string]string)
|
||||
for _, s := range pairs {
|
||||
key, value, ok := strings.Cut(s, "=")
|
||||
if !ok {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
continue
|
||||
}
|
||||
return params, errors.New("dkim: malformed header params")
|
||||
}
|
||||
|
||||
params[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func formatHeaderParams(headerFieldName string, params map[string]string) string {
|
||||
keys, bvalue, bfound := sortParams(params)
|
||||
|
||||
s := headerFieldName + ":"
|
||||
var line string
|
||||
|
||||
for _, k := range keys {
|
||||
v := params[k]
|
||||
nextLength := 3 + len(line) + len(v) + len(k)
|
||||
if nextLength > 75 {
|
||||
s += line + crlf
|
||||
line = ""
|
||||
}
|
||||
line = fmt.Sprintf("%v %v=%v;", line, k, v)
|
||||
}
|
||||
|
||||
if line != "" {
|
||||
s += line
|
||||
}
|
||||
|
||||
if bfound {
|
||||
bfiled := foldHeaderField(" b=" + bvalue)
|
||||
s += crlf + bfiled
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func sortParams(params map[string]string) ([]string, string, bool) {
|
||||
keys := make([]string, 0, len(params))
|
||||
bfound := false
|
||||
var bvalue string
|
||||
for k := range params {
|
||||
if k == "b" {
|
||||
bvalue = params["b"]
|
||||
bfound = true
|
||||
} else {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys, bvalue, bfound
|
||||
}
|
||||
|
||||
type headerPicker struct {
|
||||
h header
|
||||
picked map[string]int
|
||||
}
|
||||
|
||||
func newHeaderPicker(h header) *headerPicker {
|
||||
return &headerPicker{
|
||||
h: h,
|
||||
picked: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *headerPicker) Pick(key string) string {
|
||||
key = strings.ToLower(key)
|
||||
|
||||
at := p.picked[key]
|
||||
for i := len(p.h) - 1; i >= 0; i-- {
|
||||
kv := p.h[i]
|
||||
k, _ := parseHeaderField(kv)
|
||||
|
||||
if !strings.EqualFold(k, key) {
|
||||
continue
|
||||
}
|
||||
|
||||
if at == 0 {
|
||||
p.picked[key]++
|
||||
return kv
|
||||
}
|
||||
at--
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
184
vendor/github.com/emersion/go-msgauth/dkim/query.go
generated
vendored
Normal file
184
vendor/github.com/emersion/go-msgauth/dkim/query.go
generated
vendored
Normal file
@ -0,0 +1,184 @@
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
type verifier interface {
|
||||
Public() crypto.PublicKey
|
||||
Verify(hash crypto.Hash, hashed []byte, sig []byte) error
|
||||
}
|
||||
|
||||
type rsaVerifier struct {
|
||||
*rsa.PublicKey
|
||||
}
|
||||
|
||||
func (v rsaVerifier) Public() crypto.PublicKey {
|
||||
return v.PublicKey
|
||||
}
|
||||
|
||||
func (v rsaVerifier) Verify(hash crypto.Hash, hashed, sig []byte) error {
|
||||
return rsa.VerifyPKCS1v15(v.PublicKey, hash, hashed, sig)
|
||||
}
|
||||
|
||||
type ed25519Verifier struct {
|
||||
ed25519.PublicKey
|
||||
}
|
||||
|
||||
func (v ed25519Verifier) Public() crypto.PublicKey {
|
||||
return v.PublicKey
|
||||
}
|
||||
|
||||
func (v ed25519Verifier) Verify(hash crypto.Hash, hashed, sig []byte) error {
|
||||
if !ed25519.Verify(v.PublicKey, hashed, sig) {
|
||||
return errors.New("dkim: invalid Ed25519 signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type queryResult struct {
|
||||
Verifier verifier
|
||||
KeyAlgo string
|
||||
HashAlgos []string
|
||||
Notes string
|
||||
Services []string
|
||||
Flags []string
|
||||
}
|
||||
|
||||
// QueryMethod is a DKIM query method.
|
||||
type QueryMethod string
|
||||
|
||||
const (
|
||||
// DNS TXT resource record (RR) lookup algorithm
|
||||
QueryMethodDNSTXT QueryMethod = "dns/txt"
|
||||
)
|
||||
|
||||
type txtLookupFunc func(domain string) ([]string, error)
|
||||
type queryFunc func(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error)
|
||||
|
||||
var queryMethods = map[QueryMethod]queryFunc{
|
||||
QueryMethodDNSTXT: queryDNSTXT,
|
||||
}
|
||||
|
||||
func queryDNSTXT(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error) {
|
||||
if txtLookup == nil {
|
||||
txtLookup = net.LookupTXT
|
||||
}
|
||||
|
||||
txts, err := txtLookup(selector + "._domainkey." + domain)
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
|
||||
return nil, tempFailError("key unavailable: " + err.Error())
|
||||
} else if err != nil {
|
||||
return nil, permFailError("no key for signature: " + err.Error())
|
||||
}
|
||||
|
||||
// net.LookupTXT will concatenate strings contained in a single TXT record.
|
||||
// In other words, net.LookupTXT returns one entry per TXT record, even if
|
||||
// a record contains multiple strings.
|
||||
//
|
||||
// RFC 6376 section 3.6.2.2 says multiple TXT records lead to undefined
|
||||
// behavior, so reject that.
|
||||
switch len(txts) {
|
||||
case 0:
|
||||
return nil, permFailError("no valid key found")
|
||||
case 1:
|
||||
return parsePublicKey(txts[0])
|
||||
default:
|
||||
return nil, permFailError("multiple TXT records found for key")
|
||||
}
|
||||
}
|
||||
|
||||
func parsePublicKey(s string) (*queryResult, error) {
|
||||
params, err := parseHeaderParams(s)
|
||||
if err != nil {
|
||||
return nil, permFailError("key syntax error: " + err.Error())
|
||||
}
|
||||
|
||||
res := new(queryResult)
|
||||
|
||||
if v, ok := params["v"]; ok && v != "DKIM1" {
|
||||
return nil, permFailError("incompatible public key version")
|
||||
}
|
||||
|
||||
p, ok := params["p"]
|
||||
if !ok {
|
||||
return nil, permFailError("key syntax error: missing public key data")
|
||||
}
|
||||
if p == "" {
|
||||
return nil, permFailError("key revoked")
|
||||
}
|
||||
p = strings.ReplaceAll(p, " ", "")
|
||||
b, err := base64.StdEncoding.DecodeString(p)
|
||||
if err != nil {
|
||||
return nil, permFailError("key syntax error: " + err.Error())
|
||||
}
|
||||
switch params["k"] {
|
||||
case "rsa", "":
|
||||
pub, err := x509.ParsePKIXPublicKey(b)
|
||||
if err != nil {
|
||||
// RFC 6376 is inconsistent about whether RSA public keys should
|
||||
// be formatted as RSAPublicKey or SubjectPublicKeyInfo.
|
||||
// Erratum 3017 (https://www.rfc-editor.org/errata/eid3017) proposes
|
||||
// allowing both.
|
||||
pub, err = x509.ParsePKCS1PublicKey(b)
|
||||
if err != nil {
|
||||
return nil, permFailError("key syntax error: " + err.Error())
|
||||
}
|
||||
}
|
||||
rsaPub, ok := pub.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, permFailError("key syntax error: not an RSA public key")
|
||||
}
|
||||
// RFC 8301 section 3.2: verifiers MUST NOT consider signatures using
|
||||
// RSA keys of less than 1024 bits as valid signatures.
|
||||
if rsaPub.Size()*8 < 1024 {
|
||||
return nil, permFailError(fmt.Sprintf("key is too short: want 1024 bits, has %v bits", rsaPub.Size()*8))
|
||||
}
|
||||
res.Verifier = rsaVerifier{rsaPub}
|
||||
res.KeyAlgo = "rsa"
|
||||
case "ed25519":
|
||||
if len(b) != ed25519.PublicKeySize {
|
||||
return nil, permFailError(fmt.Sprintf("invalid Ed25519 public key size: %v bytes", len(b)))
|
||||
}
|
||||
ed25519Pub := ed25519.PublicKey(b)
|
||||
res.Verifier = ed25519Verifier{ed25519Pub}
|
||||
res.KeyAlgo = "ed25519"
|
||||
default:
|
||||
return nil, permFailError("unsupported key algorithm")
|
||||
}
|
||||
|
||||
if hashesStr, ok := params["h"]; ok {
|
||||
res.HashAlgos = parseTagList(hashesStr)
|
||||
}
|
||||
if notes, ok := params["n"]; ok {
|
||||
res.Notes = notes
|
||||
}
|
||||
if servicesStr, ok := params["s"]; ok {
|
||||
services := parseTagList(servicesStr)
|
||||
|
||||
hasWildcard := false
|
||||
for _, s := range services {
|
||||
if s == "*" {
|
||||
hasWildcard = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasWildcard {
|
||||
res.Services = services
|
||||
}
|
||||
}
|
||||
if flagsStr, ok := params["t"]; ok {
|
||||
res.Flags = parseTagList(flagsStr)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
346
vendor/github.com/emersion/go-msgauth/dkim/sign.go
generated
vendored
Normal file
346
vendor/github.com/emersion/go-msgauth/dkim/sign.go
generated
vendored
Normal file
@ -0,0 +1,346 @@
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
var randReader io.Reader = rand.Reader
|
||||
|
||||
// SignOptions is used to configure Sign. Domain, Selector and Signer are
|
||||
// mandatory.
|
||||
type SignOptions struct {
|
||||
// The SDID claiming responsibility for an introduction of a message into the
|
||||
// mail stream. Hence, the SDID value is used to form the query for the public
|
||||
// key. The SDID MUST correspond to a valid DNS name under which the DKIM key
|
||||
// record is published.
|
||||
//
|
||||
// This can't be empty.
|
||||
Domain string
|
||||
// The selector subdividing the namespace for the domain.
|
||||
//
|
||||
// This can't be empty.
|
||||
Selector string
|
||||
// The Agent or User Identifier (AUID) on behalf of which the SDID is taking
|
||||
// responsibility.
|
||||
//
|
||||
// This is optional.
|
||||
Identifier string
|
||||
|
||||
// The key used to sign the message.
|
||||
//
|
||||
// Supported Signer.Public() values are *rsa.PublicKey and
|
||||
// ed25519.PublicKey.
|
||||
Signer crypto.Signer
|
||||
// The hash algorithm used to sign the message. If zero, a default hash will
|
||||
// be chosen.
|
||||
//
|
||||
// The only supported hash algorithm is crypto.SHA256.
|
||||
Hash crypto.Hash
|
||||
|
||||
// Header and body canonicalization algorithms.
|
||||
//
|
||||
// If empty, CanonicalizationSimple is used.
|
||||
HeaderCanonicalization Canonicalization
|
||||
BodyCanonicalization Canonicalization
|
||||
|
||||
// A list of header fields to include in the signature. If nil, all headers
|
||||
// will be included. If not nil, "From" MUST be in the list.
|
||||
//
|
||||
// See RFC 6376 section 5.4.1 for recommended header fields.
|
||||
HeaderKeys []string
|
||||
|
||||
// The expiration time. A zero value means no expiration.
|
||||
Expiration time.Time
|
||||
|
||||
// A list of query methods used to retrieve the public key.
|
||||
//
|
||||
// If nil, it is implicitly defined as QueryMethodDNSTXT.
|
||||
QueryMethods []QueryMethod
|
||||
}
|
||||
|
||||
// Signer generates a DKIM signature.
|
||||
//
|
||||
// The whole message header and body must be written to the Signer. Close should
|
||||
// always be called (either after the whole message has been written, or after
|
||||
// an error occurred and the signer won't be used anymore). Close may return an
|
||||
// error in case signing fails.
|
||||
//
|
||||
// After a successful Close, Signature can be called to retrieve the
|
||||
// DKIM-Signature header field that the caller should prepend to the message.
|
||||
type Signer struct {
|
||||
pw *io.PipeWriter
|
||||
done <-chan error
|
||||
sigParams map[string]string // only valid after done received nil
|
||||
}
|
||||
|
||||
// NewSigner creates a new signer. It returns an error if SignOptions is
|
||||
// invalid.
|
||||
func NewSigner(options *SignOptions) (*Signer, error) {
|
||||
if options == nil {
|
||||
return nil, fmt.Errorf("dkim: no options specified")
|
||||
}
|
||||
if options.Domain == "" {
|
||||
return nil, fmt.Errorf("dkim: no domain specified")
|
||||
}
|
||||
if options.Selector == "" {
|
||||
return nil, fmt.Errorf("dkim: no selector specified")
|
||||
}
|
||||
if options.Signer == nil {
|
||||
return nil, fmt.Errorf("dkim: no signer specified")
|
||||
}
|
||||
|
||||
headerCan := options.HeaderCanonicalization
|
||||
if headerCan == "" {
|
||||
headerCan = CanonicalizationSimple
|
||||
}
|
||||
if _, ok := canonicalizers[headerCan]; !ok {
|
||||
return nil, fmt.Errorf("dkim: unknown header canonicalization %q", headerCan)
|
||||
}
|
||||
|
||||
bodyCan := options.BodyCanonicalization
|
||||
if bodyCan == "" {
|
||||
bodyCan = CanonicalizationSimple
|
||||
}
|
||||
if _, ok := canonicalizers[bodyCan]; !ok {
|
||||
return nil, fmt.Errorf("dkim: unknown body canonicalization %q", bodyCan)
|
||||
}
|
||||
|
||||
var keyAlgo string
|
||||
switch options.Signer.Public().(type) {
|
||||
case *rsa.PublicKey:
|
||||
keyAlgo = "rsa"
|
||||
case ed25519.PublicKey:
|
||||
keyAlgo = "ed25519"
|
||||
default:
|
||||
return nil, fmt.Errorf("dkim: unsupported key algorithm %T", options.Signer.Public())
|
||||
}
|
||||
|
||||
hash := options.Hash
|
||||
var hashAlgo string
|
||||
switch options.Hash {
|
||||
case 0: // sha256 is the default
|
||||
hash = crypto.SHA256
|
||||
fallthrough
|
||||
case crypto.SHA256:
|
||||
hashAlgo = "sha256"
|
||||
case crypto.SHA1:
|
||||
return nil, fmt.Errorf("dkim: hash algorithm too weak: sha1")
|
||||
default:
|
||||
return nil, fmt.Errorf("dkim: unsupported hash algorithm")
|
||||
}
|
||||
|
||||
if options.HeaderKeys != nil {
|
||||
ok := false
|
||||
for _, k := range options.HeaderKeys {
|
||||
if strings.EqualFold(k, "From") {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("dkim: the From header field must be signed")
|
||||
}
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
s := &Signer{
|
||||
pw: pw,
|
||||
done: done,
|
||||
}
|
||||
|
||||
closeReadWithError := func(err error) {
|
||||
pr.CloseWithError(err)
|
||||
done <- err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
// Read header
|
||||
br := bufio.NewReader(pr)
|
||||
h, err := readHeader(br)
|
||||
if err != nil {
|
||||
closeReadWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Hash body
|
||||
hasher := hash.New()
|
||||
can := canonicalizers[bodyCan].CanonicalizeBody(hasher)
|
||||
if _, err := io.Copy(can, br); err != nil {
|
||||
closeReadWithError(err)
|
||||
return
|
||||
}
|
||||
if err := can.Close(); err != nil {
|
||||
closeReadWithError(err)
|
||||
return
|
||||
}
|
||||
bodyHashed := hasher.Sum(nil)
|
||||
|
||||
params := map[string]string{
|
||||
"v": "1",
|
||||
"a": keyAlgo + "-" + hashAlgo,
|
||||
"bh": base64.StdEncoding.EncodeToString(bodyHashed),
|
||||
"c": string(headerCan) + "/" + string(bodyCan),
|
||||
"d": options.Domain,
|
||||
//"l": "", // TODO
|
||||
"s": options.Selector,
|
||||
"t": formatTime(now()),
|
||||
//"z": "", // TODO
|
||||
}
|
||||
|
||||
var headerKeys []string
|
||||
if options.HeaderKeys != nil {
|
||||
headerKeys = options.HeaderKeys
|
||||
} else {
|
||||
for _, kv := range h {
|
||||
k, _ := parseHeaderField(kv)
|
||||
headerKeys = append(headerKeys, k)
|
||||
}
|
||||
}
|
||||
params["h"] = formatTagList(headerKeys)
|
||||
|
||||
if options.Identifier != "" {
|
||||
params["i"] = options.Identifier
|
||||
}
|
||||
|
||||
if options.QueryMethods != nil {
|
||||
methods := make([]string, len(options.QueryMethods))
|
||||
for i, method := range options.QueryMethods {
|
||||
methods[i] = string(method)
|
||||
}
|
||||
params["q"] = formatTagList(methods)
|
||||
}
|
||||
|
||||
if !options.Expiration.IsZero() {
|
||||
params["x"] = formatTime(options.Expiration)
|
||||
}
|
||||
|
||||
// Hash and sign headers
|
||||
hasher.Reset()
|
||||
picker := newHeaderPicker(h)
|
||||
for _, k := range headerKeys {
|
||||
kv := picker.Pick(k)
|
||||
if kv == "" {
|
||||
// The Signer MAY include more instances of a header field name
|
||||
// in "h=" than there are actual corresponding header fields so
|
||||
// that the signature will not verify if additional header
|
||||
// fields of that name are added.
|
||||
continue
|
||||
}
|
||||
|
||||
kv = canonicalizers[headerCan].CanonicalizeHeader(kv)
|
||||
if _, err := io.WriteString(hasher, kv); err != nil {
|
||||
closeReadWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
params["b"] = ""
|
||||
sigField := formatSignature(params)
|
||||
sigField = canonicalizers[headerCan].CanonicalizeHeader(sigField)
|
||||
sigField = strings.TrimRight(sigField, crlf)
|
||||
if _, err := io.WriteString(hasher, sigField); err != nil {
|
||||
closeReadWithError(err)
|
||||
return
|
||||
}
|
||||
hashed := hasher.Sum(nil)
|
||||
|
||||
// Don't pass Hash to Sign for ed25519 as it doesn't support it
|
||||
// and will return an error ("ed25519: cannot sign hashed message").
|
||||
if keyAlgo == "ed25519" {
|
||||
hash = crypto.Hash(0)
|
||||
}
|
||||
|
||||
sig, err := options.Signer.Sign(randReader, hashed, hash)
|
||||
if err != nil {
|
||||
closeReadWithError(err)
|
||||
return
|
||||
}
|
||||
params["b"] = base64.StdEncoding.EncodeToString(sig)
|
||||
|
||||
s.sigParams = params
|
||||
closeReadWithError(nil)
|
||||
}()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Write implements io.WriteCloser.
|
||||
func (s *Signer) Write(b []byte) (n int, err error) {
|
||||
return s.pw.Write(b)
|
||||
}
|
||||
|
||||
// Close implements io.WriteCloser. The error return by Close must be checked.
|
||||
func (s *Signer) Close() error {
|
||||
if err := s.pw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return <-s.done
|
||||
}
|
||||
|
||||
// Signature returns the whole DKIM-Signature header field. It can only be
|
||||
// called after a successful Signer.Close call.
|
||||
//
|
||||
// The returned value contains both the header field name, its value and the
|
||||
// final CRLF.
|
||||
func (s *Signer) Signature() string {
|
||||
if s.sigParams == nil {
|
||||
panic("dkim: Signer.Signature must only be called after a succesful Signer.Close")
|
||||
}
|
||||
return formatSignature(s.sigParams)
|
||||
}
|
||||
|
||||
// Sign signs a message. It reads it from r and writes the signed version to w.
|
||||
func Sign(w io.Writer, r io.Reader, options *SignOptions) error {
|
||||
s, err := NewSigner(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
// We need to keep the message in a buffer so we can write the new DKIM
|
||||
// header field before the rest of the message
|
||||
var b bytes.Buffer
|
||||
mw := io.MultiWriter(&b, s)
|
||||
|
||||
if _, err := io.Copy(mw, r); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(w, s.Signature()); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, &b)
|
||||
return err
|
||||
}
|
||||
|
||||
func formatSignature(params map[string]string) string {
|
||||
sig := formatHeaderParams(headerFieldName, params)
|
||||
return sig
|
||||
}
|
||||
|
||||
func formatTagList(l []string) string {
|
||||
return strings.Join(l, ":")
|
||||
}
|
||||
|
||||
func formatTime(t time.Time) string {
|
||||
return strconv.FormatInt(t.Unix(), 10)
|
||||
}
|
462
vendor/github.com/emersion/go-msgauth/dkim/verify.go
generated
vendored
Normal file
462
vendor/github.com/emersion/go-msgauth/dkim/verify.go
generated
vendored
Normal file
@ -0,0 +1,462 @@
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type permFailError string
|
||||
|
||||
func (err permFailError) Error() string {
|
||||
return "dkim: " + string(err)
|
||||
}
|
||||
|
||||
// IsPermFail returns true if the error returned by Verify is a permanent
|
||||
// failure. A permanent failure is for instance a missing required field or a
|
||||
// malformed header.
|
||||
func IsPermFail(err error) bool {
|
||||
_, ok := err.(permFailError)
|
||||
return ok
|
||||
}
|
||||
|
||||
type tempFailError string
|
||||
|
||||
func (err tempFailError) Error() string {
|
||||
return "dkim: " + string(err)
|
||||
}
|
||||
|
||||
// IsTempFail returns true if the error returned by Verify is a temporary
|
||||
// failure.
|
||||
func IsTempFail(err error) bool {
|
||||
_, ok := err.(tempFailError)
|
||||
return ok
|
||||
}
|
||||
|
||||
type failError string
|
||||
|
||||
func (err failError) Error() string {
|
||||
return "dkim: " + string(err)
|
||||
}
|
||||
|
||||
// isFail returns true if the error returned by Verify is a signature error.
|
||||
func isFail(err error) bool {
|
||||
_, ok := err.(failError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ErrTooManySignatures is returned by Verify when the message exceeds the
|
||||
// maximum number of signatures.
|
||||
var ErrTooManySignatures = errors.New("dkim: too many signatures")
|
||||
|
||||
var requiredTags = []string{"v", "a", "b", "bh", "d", "h", "s"}
|
||||
|
||||
// A Verification is produced by Verify when it checks if one signature is
|
||||
// valid. If the signature is valid, Err is nil.
|
||||
type Verification struct {
|
||||
// The SDID claiming responsibility for an introduction of a message into the
|
||||
// mail stream.
|
||||
Domain string
|
||||
// The Agent or User Identifier (AUID) on behalf of which the SDID is taking
|
||||
// responsibility.
|
||||
Identifier string
|
||||
|
||||
// The list of signed header fields.
|
||||
HeaderKeys []string
|
||||
|
||||
// The time that this signature was created. If unknown, it's set to zero.
|
||||
Time time.Time
|
||||
// The expiration time. If the signature doesn't expire, it's set to zero.
|
||||
Expiration time.Time
|
||||
|
||||
// Err is nil if the signature is valid.
|
||||
Err error
|
||||
}
|
||||
|
||||
type signature struct {
|
||||
i int
|
||||
v string
|
||||
}
|
||||
|
||||
// VerifyOptions allows to customize the default signature verification
|
||||
// behavior.
|
||||
type VerifyOptions struct {
|
||||
// LookupTXT returns the DNS TXT records for the given domain name. If nil,
|
||||
// net.LookupTXT is used.
|
||||
LookupTXT func(domain string) ([]string, error)
|
||||
// MaxVerifications controls the maximum number of signature verifications
|
||||
// to perform. If more signatures are present, the first MaxVerifications
|
||||
// signatures are verified, the rest are ignored and ErrTooManySignatures
|
||||
// is returned. If zero, there is no maximum.
|
||||
MaxVerifications int
|
||||
}
|
||||
|
||||
// Verify checks if a message's signatures are valid. It returns one
|
||||
// verification per signature.
|
||||
//
|
||||
// There is no guarantee that the reader will be completely consumed.
|
||||
func Verify(r io.Reader) ([]*Verification, error) {
|
||||
return VerifyWithOptions(r, nil)
|
||||
}
|
||||
|
||||
// VerifyWithOptions performs the same task as Verify, but allows specifying
|
||||
// verification options.
|
||||
func VerifyWithOptions(r io.Reader, options *VerifyOptions) ([]*Verification, error) {
|
||||
// Read header
|
||||
bufr := bufio.NewReader(r)
|
||||
h, err := readHeader(bufr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Scan header fields for signatures
|
||||
var signatures []*signature
|
||||
for i, kv := range h {
|
||||
k, v := parseHeaderField(kv)
|
||||
if strings.EqualFold(k, headerFieldName) {
|
||||
signatures = append(signatures, &signature{i, v})
|
||||
}
|
||||
}
|
||||
|
||||
tooManySignatures := false
|
||||
if options != nil && options.MaxVerifications > 0 && len(signatures) > options.MaxVerifications {
|
||||
tooManySignatures = true
|
||||
signatures = signatures[:options.MaxVerifications]
|
||||
}
|
||||
|
||||
var verifs []*Verification
|
||||
if len(signatures) == 1 {
|
||||
// If there is only one signature - just verify it.
|
||||
v, err := verify(h, bufr, h[signatures[0].i], signatures[0].v, options)
|
||||
if err != nil && !IsTempFail(err) && !IsPermFail(err) && !isFail(err) {
|
||||
return nil, err
|
||||
}
|
||||
v.Err = err
|
||||
verifs = []*Verification{v}
|
||||
} else {
|
||||
verifs, err = parallelVerify(bufr, h, signatures, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if tooManySignatures {
|
||||
return verifs, ErrTooManySignatures
|
||||
}
|
||||
return verifs, nil
|
||||
}
|
||||
|
||||
func parallelVerify(r io.Reader, h header, signatures []*signature, options *VerifyOptions) ([]*Verification, error) {
|
||||
pipeWriters := make([]*io.PipeWriter, len(signatures))
|
||||
// We can't pass pipeWriter to io.MultiWriter directly,
|
||||
// we need a slice of io.Writer, but we also need *io.PipeWriter
|
||||
// to call Close on it.
|
||||
writers := make([]io.Writer, len(signatures))
|
||||
chans := make([]chan *Verification, len(signatures))
|
||||
|
||||
for i, sig := range signatures {
|
||||
// Be careful with loop variables and goroutines.
|
||||
i, sig := i, sig
|
||||
|
||||
chans[i] = make(chan *Verification, 1)
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
writers[i] = pw
|
||||
pipeWriters[i] = pw
|
||||
|
||||
go func() {
|
||||
v, err := verify(h, pr, h[sig.i], sig.v, options)
|
||||
|
||||
// Make sure we consume the whole reader, otherwise io.Copy on
|
||||
// other side can block forever.
|
||||
io.Copy(ioutil.Discard, pr)
|
||||
|
||||
v.Err = err
|
||||
chans[i] <- v
|
||||
}()
|
||||
}
|
||||
|
||||
if _, err := io.Copy(io.MultiWriter(writers...), r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, wr := range pipeWriters {
|
||||
wr.Close()
|
||||
}
|
||||
|
||||
verifications := make([]*Verification, len(signatures))
|
||||
for i, ch := range chans {
|
||||
verifications[i] = <-ch
|
||||
}
|
||||
|
||||
// Return unexpected failures as a separate error.
|
||||
for _, v := range verifications {
|
||||
err := v.Err
|
||||
if err != nil && !IsTempFail(err) && !IsPermFail(err) && !isFail(err) {
|
||||
v.Err = nil
|
||||
return verifications, err
|
||||
}
|
||||
}
|
||||
return verifications, nil
|
||||
}
|
||||
|
||||
func verify(h header, r io.Reader, sigField, sigValue string, options *VerifyOptions) (*Verification, error) {
|
||||
verif := new(Verification)
|
||||
|
||||
params, err := parseHeaderParams(sigValue)
|
||||
if err != nil {
|
||||
return verif, permFailError("malformed signature tags: " + err.Error())
|
||||
}
|
||||
|
||||
if params["v"] != "1" {
|
||||
return verif, permFailError("incompatible signature version")
|
||||
}
|
||||
|
||||
verif.Domain = stripWhitespace(params["d"])
|
||||
|
||||
for _, tag := range requiredTags {
|
||||
if _, ok := params[tag]; !ok {
|
||||
return verif, permFailError("signature missing required tag")
|
||||
}
|
||||
}
|
||||
|
||||
if i, ok := params["i"]; ok {
|
||||
verif.Identifier = stripWhitespace(i)
|
||||
if !strings.HasSuffix(verif.Identifier, "@"+verif.Domain) && !strings.HasSuffix(verif.Identifier, "."+verif.Domain) {
|
||||
return verif, permFailError("domain mismatch")
|
||||
}
|
||||
} else {
|
||||
verif.Identifier = "@" + verif.Domain
|
||||
}
|
||||
|
||||
headerKeys := parseTagList(params["h"])
|
||||
ok := false
|
||||
for _, k := range headerKeys {
|
||||
if strings.EqualFold(k, "from") {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return verif, permFailError("From field not signed")
|
||||
}
|
||||
verif.HeaderKeys = headerKeys
|
||||
|
||||
if timeStr, ok := params["t"]; ok {
|
||||
t, err := parseTime(timeStr)
|
||||
if err != nil {
|
||||
return verif, permFailError("malformed time: " + err.Error())
|
||||
}
|
||||
verif.Time = t
|
||||
}
|
||||
if expiresStr, ok := params["x"]; ok {
|
||||
t, err := parseTime(expiresStr)
|
||||
if err != nil {
|
||||
return verif, permFailError("malformed expiration time: " + err.Error())
|
||||
}
|
||||
verif.Expiration = t
|
||||
if now().After(t) {
|
||||
return verif, permFailError("signature has expired")
|
||||
}
|
||||
}
|
||||
|
||||
// Query public key
|
||||
// TODO: compute hash in parallel
|
||||
methods := []string{string(QueryMethodDNSTXT)}
|
||||
if methodsStr, ok := params["q"]; ok {
|
||||
methods = parseTagList(methodsStr)
|
||||
}
|
||||
var res *queryResult
|
||||
for _, method := range methods {
|
||||
if query, ok := queryMethods[QueryMethod(method)]; ok {
|
||||
if options != nil {
|
||||
res, err = query(verif.Domain, stripWhitespace(params["s"]), options.LookupTXT)
|
||||
} else {
|
||||
res, err = query(verif.Domain, stripWhitespace(params["s"]), nil)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return verif, err
|
||||
} else if res == nil {
|
||||
return verif, permFailError("unsupported public key query method")
|
||||
}
|
||||
|
||||
// Parse algos
|
||||
keyAlgo, hashAlgo, ok := strings.Cut(stripWhitespace(params["a"]), "-")
|
||||
if !ok {
|
||||
return verif, permFailError("malformed algorithm name")
|
||||
}
|
||||
|
||||
// Check hash algo
|
||||
if res.HashAlgos != nil {
|
||||
ok := false
|
||||
for _, algo := range res.HashAlgos {
|
||||
if algo == hashAlgo {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return verif, permFailError("inappropriate hash algorithm")
|
||||
}
|
||||
}
|
||||
var hash crypto.Hash
|
||||
switch hashAlgo {
|
||||
case "sha1":
|
||||
// RFC 8301 section 3.1: rsa-sha1 MUST NOT be used for signing or
|
||||
// verifying.
|
||||
return verif, permFailError(fmt.Sprintf("hash algorithm too weak: %v", hashAlgo))
|
||||
case "sha256":
|
||||
hash = crypto.SHA256
|
||||
default:
|
||||
return verif, permFailError("unsupported hash algorithm")
|
||||
}
|
||||
|
||||
// Check key algo
|
||||
if res.KeyAlgo != keyAlgo {
|
||||
return verif, permFailError("inappropriate key algorithm")
|
||||
}
|
||||
|
||||
if res.Services != nil {
|
||||
ok := false
|
||||
for _, s := range res.Services {
|
||||
if s == "email" {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return verif, permFailError("inappropriate service")
|
||||
}
|
||||
}
|
||||
|
||||
headerCan, bodyCan := parseCanonicalization(params["c"])
|
||||
if _, ok := canonicalizers[headerCan]; !ok {
|
||||
return verif, permFailError("unsupported header canonicalization algorithm")
|
||||
}
|
||||
if _, ok := canonicalizers[bodyCan]; !ok {
|
||||
return verif, permFailError("unsupported body canonicalization algorithm")
|
||||
}
|
||||
|
||||
// The body length "l" parameter is insecure, because it allows parts of
|
||||
// the message body to not be signed. Reject messages which have it set.
|
||||
if _, ok := params["l"]; ok {
|
||||
// TODO: technically should be policyError
|
||||
return verif, failError("message contains an insecure body length tag")
|
||||
}
|
||||
|
||||
// Parse body hash and signature
|
||||
bodyHashed, err := decodeBase64String(params["bh"])
|
||||
if err != nil {
|
||||
return verif, permFailError("malformed body hash: " + err.Error())
|
||||
}
|
||||
sig, err := decodeBase64String(params["b"])
|
||||
if err != nil {
|
||||
return verif, permFailError("malformed signature: " + err.Error())
|
||||
}
|
||||
|
||||
// Check body hash
|
||||
hasher := hash.New()
|
||||
wc := canonicalizers[bodyCan].CanonicalizeBody(hasher)
|
||||
if _, err := io.Copy(wc, r); err != nil {
|
||||
return verif, err
|
||||
}
|
||||
if err := wc.Close(); err != nil {
|
||||
return verif, err
|
||||
}
|
||||
if subtle.ConstantTimeCompare(hasher.Sum(nil), bodyHashed) != 1 {
|
||||
return verif, failError("body hash did not verify")
|
||||
}
|
||||
|
||||
// Compute data hash
|
||||
hasher.Reset()
|
||||
picker := newHeaderPicker(h)
|
||||
for _, key := range headerKeys {
|
||||
kv := picker.Pick(key)
|
||||
if kv == "" {
|
||||
// The field MAY contain names of header fields that do not exist
|
||||
// when signed; nonexistent header fields do not contribute to the
|
||||
// signature computation
|
||||
continue
|
||||
}
|
||||
|
||||
kv = canonicalizers[headerCan].CanonicalizeHeader(kv)
|
||||
if _, err := hasher.Write([]byte(kv)); err != nil {
|
||||
return verif, err
|
||||
}
|
||||
}
|
||||
canSigField := removeSignature(sigField)
|
||||
canSigField = canonicalizers[headerCan].CanonicalizeHeader(canSigField)
|
||||
canSigField = strings.TrimRight(canSigField, "\r\n")
|
||||
if _, err := hasher.Write([]byte(canSigField)); err != nil {
|
||||
return verif, err
|
||||
}
|
||||
hashed := hasher.Sum(nil)
|
||||
|
||||
// Check signature
|
||||
if err := res.Verifier.Verify(hash, hashed, sig); err != nil {
|
||||
return verif, failError("signature did not verify: " + err.Error())
|
||||
}
|
||||
|
||||
return verif, nil
|
||||
}
|
||||
|
||||
func parseTagList(s string) []string {
|
||||
tags := strings.Split(s, ":")
|
||||
for i, t := range tags {
|
||||
tags[i] = stripWhitespace(t)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func parseCanonicalization(s string) (headerCan, bodyCan Canonicalization) {
|
||||
headerCan = CanonicalizationSimple
|
||||
bodyCan = CanonicalizationSimple
|
||||
|
||||
cans := strings.SplitN(stripWhitespace(s), "/", 2)
|
||||
if cans[0] != "" {
|
||||
headerCan = Canonicalization(cans[0])
|
||||
}
|
||||
if len(cans) > 1 {
|
||||
bodyCan = Canonicalization(cans[1])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseTime(s string) (time.Time, error) {
|
||||
sec, err := strconv.ParseInt(stripWhitespace(s), 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Unix(sec, 0), nil
|
||||
}
|
||||
|
||||
func decodeBase64String(s string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(stripWhitespace(s))
|
||||
}
|
||||
|
||||
func stripWhitespace(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
}
|
||||
|
||||
var sigRegex = regexp.MustCompile(`(b\s*=)[^;]+`)
|
||||
|
||||
func removeSignature(s string) string {
|
||||
return sigRegex.ReplaceAllString(s, "$1")
|
||||
}
|
24
vendor/github.com/toorop/go-dkim/.gitignore
generated
vendored
24
vendor/github.com/toorop/go-dkim/.gitignore
generated
vendored
@ -1,24 +0,0 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
56
vendor/github.com/toorop/go-dkim/README.md
generated
vendored
56
vendor/github.com/toorop/go-dkim/README.md
generated
vendored
@ -1,56 +0,0 @@
|
||||
# go-dkim
|
||||
DKIM package for Golang
|
||||
|
||||
[](https://godoc.org/github.com/toorop/go-dkim)
|
||||
|
||||
## Getting started
|
||||
|
||||
### Install
|
||||
```
|
||||
go get github.com/toorop/go-dkim
|
||||
```
|
||||
Warning: you need to use Go 1.4.2-master or 1.4.3 (when it will be available)
|
||||
see https://github.com/golang/go/issues/10482 fro more info.
|
||||
|
||||
### Sign email
|
||||
|
||||
```go
|
||||
import (
|
||||
dkim "github.com/toorop/go-dkim"
|
||||
)
|
||||
|
||||
func main(){
|
||||
// email is the email to sign (byte slice)
|
||||
// privateKey the private key (pem encoded, byte slice )
|
||||
options := dkim.NewSigOptions()
|
||||
options.PrivateKey = privateKey
|
||||
options.Domain = "mydomain.tld"
|
||||
options.Selector = "myselector"
|
||||
options.SignatureExpireIn = 3600
|
||||
options.BodyLength = 50
|
||||
options.Headers = []string{"from", "date", "mime-version", "received", "received"}
|
||||
options.AddSignatureTimestamp = true
|
||||
options.Canonicalization = "relaxed/relaxed"
|
||||
err := dkim.Sign(&email, options)
|
||||
// handle err..
|
||||
|
||||
// And... that's it, 'email' is signed ! Amazing© !!!
|
||||
}
|
||||
```
|
||||
|
||||
### Verify
|
||||
```go
|
||||
import (
|
||||
dkim "github.com/toorop/go-dkim"
|
||||
)
|
||||
|
||||
func main(){
|
||||
// email is the email to verify (byte slice)
|
||||
status, err := Verify(&email)
|
||||
// handle status, err (see godoc for status)
|
||||
}
|
||||
```
|
||||
|
||||
## Todo
|
||||
|
||||
- [ ] handle z tag (copied header fields used for diagnostic use)
|
564
vendor/github.com/toorop/go-dkim/dkim.go
generated
vendored
564
vendor/github.com/toorop/go-dkim/dkim.go
generated
vendored
@ -1,564 +0,0 @@
|
||||
// Package dkim provides tools for signing and verify a email according to RFC 6376
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"hash"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CRLF = "\r\n"
|
||||
TAB = " "
|
||||
FWS = CRLF + TAB
|
||||
MaxHeaderLineLength = 70
|
||||
)
|
||||
|
||||
type verifyOutput int
|
||||
|
||||
const (
|
||||
SUCCESS verifyOutput = 1 + iota
|
||||
PERMFAIL
|
||||
TEMPFAIL
|
||||
NOTSIGNED
|
||||
TESTINGSUCCESS
|
||||
TESTINGPERMFAIL
|
||||
TESTINGTEMPFAIL
|
||||
)
|
||||
|
||||
// sigOptions represents signing options
|
||||
type SigOptions struct {
|
||||
|
||||
// DKIM version (default 1)
|
||||
Version uint
|
||||
|
||||
// Private key used for signing (required)
|
||||
PrivateKey []byte
|
||||
|
||||
// Domain (required)
|
||||
Domain string
|
||||
|
||||
// Selector (required)
|
||||
Selector string
|
||||
|
||||
// The Agent of User IDentifier
|
||||
Auid string
|
||||
|
||||
// Message canonicalization (plain-text; OPTIONAL, default is
|
||||
// "simple/simple"). This tag informs the Verifier of the type of
|
||||
// canonicalization used to prepare the message for signing.
|
||||
Canonicalization string
|
||||
|
||||
// The algorithm used to generate the signature
|
||||
//"rsa-sha1" or "rsa-sha256"
|
||||
Algo string
|
||||
|
||||
// Signed header fields
|
||||
Headers []string
|
||||
|
||||
// Body length count( if set to 0 this tag is ommited in Dkim header)
|
||||
BodyLength uint
|
||||
|
||||
// Query Methods used to retrieve the public key
|
||||
QueryMethods []string
|
||||
|
||||
// Add a signature timestamp
|
||||
AddSignatureTimestamp bool
|
||||
|
||||
// Time validity of the signature (0=never)
|
||||
SignatureExpireIn uint64
|
||||
|
||||
// CopiedHeaderFileds
|
||||
CopiedHeaderFields []string
|
||||
}
|
||||
|
||||
// NewSigOptions returns new sigoption with some defaults value
|
||||
func NewSigOptions() SigOptions {
|
||||
return SigOptions{
|
||||
Version: 1,
|
||||
Canonicalization: "simple/simple",
|
||||
Algo: "rsa-sha256",
|
||||
Headers: []string{"from"},
|
||||
BodyLength: 0,
|
||||
QueryMethods: []string{"dns/txt"},
|
||||
AddSignatureTimestamp: true,
|
||||
SignatureExpireIn: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Sign signs an email
|
||||
func Sign(email *[]byte, options SigOptions) error {
|
||||
var privateKey *rsa.PrivateKey
|
||||
var err error
|
||||
|
||||
// PrivateKey
|
||||
if len(options.PrivateKey) == 0 {
|
||||
return ErrSignPrivateKeyRequired
|
||||
}
|
||||
d, _ := pem.Decode(options.PrivateKey)
|
||||
if d == nil {
|
||||
return ErrCandNotParsePrivateKey
|
||||
}
|
||||
|
||||
// try to parse it as PKCS1 otherwise try PKCS8
|
||||
if key, err := x509.ParsePKCS1PrivateKey(d.Bytes); err != nil {
|
||||
if key, err := x509.ParsePKCS8PrivateKey(d.Bytes); err != nil {
|
||||
return ErrCandNotParsePrivateKey
|
||||
} else {
|
||||
privateKey = key.(*rsa.PrivateKey)
|
||||
}
|
||||
} else {
|
||||
privateKey = key
|
||||
}
|
||||
|
||||
// Domain required
|
||||
if options.Domain == "" {
|
||||
return ErrSignDomainRequired
|
||||
}
|
||||
|
||||
// Selector required
|
||||
if options.Selector == "" {
|
||||
return ErrSignSelectorRequired
|
||||
}
|
||||
|
||||
// Canonicalization
|
||||
options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Algo
|
||||
options.Algo = strings.ToLower(options.Algo)
|
||||
if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" {
|
||||
return ErrSignBadAlgo
|
||||
}
|
||||
|
||||
// Header must contain "from"
|
||||
hasFrom := false
|
||||
for i, h := range options.Headers {
|
||||
h = strings.ToLower(h)
|
||||
options.Headers[i] = h
|
||||
if h == "from" {
|
||||
hasFrom = true
|
||||
}
|
||||
}
|
||||
if !hasFrom {
|
||||
return ErrSignHeaderShouldContainsFrom
|
||||
}
|
||||
|
||||
// Normalize
|
||||
headers, body, err := canonicalize(email, options.Canonicalization, options.Headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signHash := strings.Split(options.Algo, "-")
|
||||
|
||||
// hash body
|
||||
bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get dkim header base
|
||||
dkimHeader := newDkimHeaderBySigOptions(options)
|
||||
dHeader := dkimHeader.getHeaderBaseForSigning(bodyHash)
|
||||
|
||||
canonicalizations := strings.Split(options.Canonicalization, "/")
|
||||
dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headers = append(headers, []byte(dHeaderCanonicalized)...)
|
||||
headers = bytes.TrimRight(headers, " \r\n")
|
||||
|
||||
// sign
|
||||
sig, err := getSignature(&headers, privateKey, signHash[1])
|
||||
|
||||
// add to DKIM-Header
|
||||
subh := ""
|
||||
l := len(subh)
|
||||
for _, c := range sig {
|
||||
subh += string(c)
|
||||
l++
|
||||
if l >= MaxHeaderLineLength {
|
||||
dHeader += subh + FWS
|
||||
subh = ""
|
||||
l = 0
|
||||
}
|
||||
}
|
||||
dHeader += subh + CRLF
|
||||
*email = append([]byte(dHeader), *email...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify verifies an email an return
|
||||
// state: SUCCESS or PERMFAIL or TEMPFAIL, TESTINGSUCCESS, TESTINGPERMFAIL
|
||||
// TESTINGTEMPFAIL or NOTSIGNED
|
||||
// error: if an error occurs during verification
|
||||
func Verify(email *[]byte, opts ...DNSOpt) (verifyOutput, error) {
|
||||
// parse email
|
||||
dkimHeader, err := GetHeader(email)
|
||||
if err != nil {
|
||||
if err == ErrDkimHeaderNotFound {
|
||||
return NOTSIGNED, ErrDkimHeaderNotFound
|
||||
}
|
||||
return PERMFAIL, err
|
||||
}
|
||||
|
||||
// we do not set query method because if it's others, validation failed earlier
|
||||
pubKey, verifyOutputOnError, err := NewPubKeyRespFromDNS(dkimHeader.Selector, dkimHeader.Domain, opts...)
|
||||
if err != nil {
|
||||
// fix https://github.com/toorop/go-dkim/issues/1
|
||||
//return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting)
|
||||
return verifyOutputOnError, err
|
||||
}
|
||||
|
||||
// Normalize
|
||||
headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers)
|
||||
if err != nil {
|
||||
return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting)
|
||||
}
|
||||
sigHash := strings.Split(dkimHeader.Algorithm, "-")
|
||||
// check if hash algo are compatible
|
||||
compatible := false
|
||||
for _, algo := range pubKey.HashAlgo {
|
||||
if sigHash[1] == algo {
|
||||
compatible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !compatible {
|
||||
return getVerifyOutput(PERMFAIL, ErrVerifyInappropriateHashAlgo, pubKey.FlagTesting)
|
||||
}
|
||||
|
||||
// expired ?
|
||||
if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() {
|
||||
return getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting)
|
||||
|
||||
}
|
||||
|
||||
//println("|" + string(body) + "|")
|
||||
// get body hash
|
||||
bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength)
|
||||
if err != nil {
|
||||
return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting)
|
||||
}
|
||||
//println(bodyHash)
|
||||
if bodyHash != dkimHeader.BodyHash {
|
||||
return getVerifyOutput(PERMFAIL, ErrVerifyBodyHash, pubKey.FlagTesting)
|
||||
}
|
||||
|
||||
// compute sig
|
||||
dkimHeaderCano, err := canonicalizeHeader(dkimHeader.rawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0])
|
||||
if err != nil {
|
||||
return getVerifyOutput(TEMPFAIL, err, pubKey.FlagTesting)
|
||||
}
|
||||
toSignStr := string(headers) + dkimHeaderCano
|
||||
toSign := bytes.TrimRight([]byte(toSignStr), " \r\n")
|
||||
|
||||
err = verifySignature(toSign, dkimHeader.SignatureData, &pubKey.PubKey, sigHash[1])
|
||||
if err != nil {
|
||||
return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting)
|
||||
}
|
||||
return SUCCESS, nil
|
||||
}
|
||||
|
||||
// getVerifyOutput returns output of verify fct according to the testing flag
|
||||
func getVerifyOutput(status verifyOutput, err error, flagTesting bool) (verifyOutput, error) {
|
||||
if !flagTesting {
|
||||
return status, err
|
||||
}
|
||||
switch status {
|
||||
case SUCCESS:
|
||||
return TESTINGSUCCESS, err
|
||||
case PERMFAIL:
|
||||
return TESTINGPERMFAIL, err
|
||||
case TEMPFAIL:
|
||||
return TESTINGTEMPFAIL, err
|
||||
}
|
||||
// should never happen but compilator sream whithout return
|
||||
return status, err
|
||||
}
|
||||
|
||||
// canonicalize returns canonicalized version of header and body
|
||||
func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, err error) {
|
||||
body = []byte{}
|
||||
rxReduceWS := regexp.MustCompile(`[ \t]+`)
|
||||
|
||||
rawHeaders, rawBody, err := getHeadersBody(email)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
canonicalizations := strings.Split(cano, "/")
|
||||
|
||||
// canonicalyze header
|
||||
headersList, err := getHeadersList(&rawHeaders)
|
||||
|
||||
// pour chaque header a conserver on traverse tous les headers dispo
|
||||
// If multi instance of a field we must keep it from the bottom to the top
|
||||
var match *list.Element
|
||||
headersToKeepList := list.New()
|
||||
|
||||
for _, headerToKeep := range h {
|
||||
match = nil
|
||||
headerToKeepToLower := strings.ToLower(headerToKeep)
|
||||
for e := headersList.Front(); e != nil; e = e.Next() {
|
||||
//fmt.Printf("|%s|\n", e.Value.(string))
|
||||
t := strings.Split(e.Value.(string), ":")
|
||||
if strings.ToLower(t[0]) == headerToKeepToLower {
|
||||
match = e
|
||||
}
|
||||
}
|
||||
if match != nil {
|
||||
headersToKeepList.PushBack(match.Value.(string) + "\r\n")
|
||||
headersList.Remove(match)
|
||||
}
|
||||
}
|
||||
|
||||
//if canonicalizations[0] == "simple" {
|
||||
for e := headersToKeepList.Front(); e != nil; e = e.Next() {
|
||||
cHeader, err := canonicalizeHeader(e.Value.(string), canonicalizations[0])
|
||||
if err != nil {
|
||||
return headers, body, err
|
||||
}
|
||||
headers = append(headers, []byte(cHeader)...)
|
||||
}
|
||||
// canonicalyze body
|
||||
if canonicalizations[1] == "simple" {
|
||||
// simple
|
||||
// The "simple" body canonicalization algorithm ignores all empty lines
|
||||
// at the end of the message body. An empty line is a line of zero
|
||||
// length after removal of the line terminator. If there is no body or
|
||||
// no trailing CRLF on the message body, a CRLF is added. It makes no
|
||||
// other changes to the message body. In more formal terms, the
|
||||
// "simple" body canonicalization algorithm converts "*CRLF" at the end
|
||||
// of the body to a single "CRLF".
|
||||
// Note that a completely empty or missing body is canonicalized as a
|
||||
// single "CRLF"; that is, the canonicalized length will be 2 octets.
|
||||
body = bytes.TrimRight(rawBody, "\r\n")
|
||||
body = append(body, []byte{13, 10}...)
|
||||
} else {
|
||||
// relaxed
|
||||
// Ignore all whitespace at the end of lines. Implementations
|
||||
// MUST NOT remove the CRLF at the end of the line.
|
||||
// Reduce all sequences of WSP within a line to a single SP
|
||||
// character.
|
||||
// Ignore all empty lines at the end of the message body. "Empty
|
||||
// line" is defined in Section 3.4.3. If the body is non-empty but
|
||||
// does not end with a CRLF, a CRLF is added. (For email, this is
|
||||
// only possible when using extensions to SMTP or non-SMTP transport
|
||||
// mechanisms.)
|
||||
rawBody = rxReduceWS.ReplaceAll(rawBody, []byte(" "))
|
||||
for _, line := range bytes.SplitAfter(rawBody, []byte{10}) {
|
||||
line = bytes.TrimRight(line, " \r\n")
|
||||
body = append(body, line...)
|
||||
body = append(body, []byte{13, 10}...)
|
||||
}
|
||||
body = bytes.TrimRight(body, "\r\n")
|
||||
body = append(body, []byte{13, 10}...)
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// canonicalizeHeader returns canonicalized version of header
|
||||
func canonicalizeHeader(header string, algo string) (string, error) {
|
||||
//rxReduceWS := regexp.MustCompile(`[ \t]+`)
|
||||
if algo == "simple" {
|
||||
// The "simple" header canonicalization algorithm does not change header
|
||||
// fields in any way. Header fields MUST be presented to the signing or
|
||||
// verification algorithm exactly as they are in the message being
|
||||
// signed or verified. In particular, header field names MUST NOT be
|
||||
// case folded and whitespace MUST NOT be changed.
|
||||
return header, nil
|
||||
} else if algo == "relaxed" {
|
||||
// The "relaxed" header canonicalization algorithm MUST apply the
|
||||
// following steps in order:
|
||||
|
||||
// Convert all header field names (not the header field values) to
|
||||
// lowercase. For example, convert "SUBJect: AbC" to "subject: AbC".
|
||||
|
||||
// Unfold all header field continuation lines as described in
|
||||
// [RFC5322]; in particular, lines with terminators embedded in
|
||||
// continued header field values (that is, CRLF sequences followed by
|
||||
// WSP) MUST be interpreted without the CRLF. Implementations MUST
|
||||
// NOT remove the CRLF at the end of the header field value.
|
||||
|
||||
// Convert all sequences of one or more WSP characters to a single SP
|
||||
// character. WSP characters here include those before and after a
|
||||
// line folding boundary.
|
||||
|
||||
// Delete all WSP characters at the end of each unfolded header field
|
||||
// value.
|
||||
|
||||
// Delete any WSP characters remaining before and after the colon
|
||||
// separating the header field name from the header field value. The
|
||||
// colon separator MUST be retained.
|
||||
kv := strings.SplitN(header, ":", 2)
|
||||
if len(kv) != 2 {
|
||||
return header, ErrBadMailFormatHeaders
|
||||
}
|
||||
k := strings.ToLower(kv[0])
|
||||
k = strings.TrimSpace(k)
|
||||
v := removeFWS(kv[1])
|
||||
//v = rxReduceWS.ReplaceAllString(v, " ")
|
||||
//v = strings.TrimSpace(v)
|
||||
return k + ":" + v + CRLF, nil
|
||||
}
|
||||
return header, ErrSignBadCanonicalization
|
||||
}
|
||||
|
||||
// getBodyHash return the hash (bas64encoded) of the body
|
||||
func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) {
|
||||
var h hash.Hash
|
||||
if algo == "sha1" {
|
||||
h = sha1.New()
|
||||
} else {
|
||||
h = sha256.New()
|
||||
}
|
||||
toH := *body
|
||||
// if l tag (body length)
|
||||
if bodyLength != 0 {
|
||||
if uint(len(toH)) < bodyLength {
|
||||
return "", ErrBadDKimTagLBodyTooShort
|
||||
}
|
||||
toH = toH[0:bodyLength]
|
||||
}
|
||||
|
||||
h.Write(toH)
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// getSignature return signature of toSign using key
|
||||
func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, error) {
|
||||
var h1 hash.Hash
|
||||
var h2 crypto.Hash
|
||||
switch algo {
|
||||
case "sha1":
|
||||
h1 = sha1.New()
|
||||
h2 = crypto.SHA1
|
||||
break
|
||||
case "sha256":
|
||||
h1 = sha256.New()
|
||||
h2 = crypto.SHA256
|
||||
break
|
||||
default:
|
||||
return "", ErrVerifyInappropriateHashAlgo
|
||||
}
|
||||
|
||||
// sign
|
||||
h1.Write(*toSign)
|
||||
sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(sig), nil
|
||||
}
|
||||
|
||||
// verifySignature verify signature from pubkey
|
||||
func verifySignature(toSign []byte, sig64 string, key *rsa.PublicKey, algo string) error {
|
||||
var h1 hash.Hash
|
||||
var h2 crypto.Hash
|
||||
switch algo {
|
||||
case "sha1":
|
||||
h1 = sha1.New()
|
||||
h2 = crypto.SHA1
|
||||
break
|
||||
case "sha256":
|
||||
h1 = sha256.New()
|
||||
h2 = crypto.SHA256
|
||||
break
|
||||
default:
|
||||
return ErrVerifyInappropriateHashAlgo
|
||||
}
|
||||
|
||||
h1.Write(toSign)
|
||||
sig, err := base64.StdEncoding.DecodeString(sig64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig)
|
||||
}
|
||||
|
||||
// removeFWS removes all FWS from string
|
||||
func removeFWS(in string) string {
|
||||
rxReduceWS := regexp.MustCompile(`[ \t]+`)
|
||||
out := strings.Replace(in, "\n", "", -1)
|
||||
out = strings.Replace(out, "\r", "", -1)
|
||||
out = rxReduceWS.ReplaceAllString(out, " ")
|
||||
return strings.TrimSpace(out)
|
||||
}
|
||||
|
||||
// validateCanonicalization validate canonicalization (c flag)
|
||||
func validateCanonicalization(cano string) (string, error) {
|
||||
p := strings.Split(cano, "/")
|
||||
if len(p) > 2 {
|
||||
return "", ErrSignBadCanonicalization
|
||||
}
|
||||
if len(p) == 1 {
|
||||
cano = cano + "/simple"
|
||||
}
|
||||
for _, c := range p {
|
||||
if c != "simple" && c != "relaxed" {
|
||||
return "", ErrSignBadCanonicalization
|
||||
}
|
||||
}
|
||||
return cano, nil
|
||||
}
|
||||
|
||||
// getHeadersList returns headers as list
|
||||
func getHeadersList(rawHeader *[]byte) (*list.List, error) {
|
||||
headersList := list.New()
|
||||
currentHeader := []byte{}
|
||||
for _, line := range bytes.SplitAfter(*rawHeader, []byte{10}) {
|
||||
if line[0] == 32 || line[0] == 9 {
|
||||
if len(currentHeader) == 0 {
|
||||
return headersList, ErrBadMailFormatHeaders
|
||||
}
|
||||
currentHeader = append(currentHeader, line...)
|
||||
} else {
|
||||
// New header, save current if exists
|
||||
if len(currentHeader) != 0 {
|
||||
headersList.PushBack(string(bytes.TrimRight(currentHeader, "\r\n")))
|
||||
currentHeader = []byte{}
|
||||
}
|
||||
currentHeader = append(currentHeader, line...)
|
||||
}
|
||||
}
|
||||
headersList.PushBack(string(currentHeader))
|
||||
return headersList, nil
|
||||
}
|
||||
|
||||
// getHeadersBody return headers and body
|
||||
func getHeadersBody(email *[]byte) ([]byte, []byte, error) {
|
||||
substitutedEmail := *email
|
||||
|
||||
// only replace \n with \r\n when \r\n\r\n not exists
|
||||
if bytes.Index(*email, []byte{13, 10, 13, 10}) < 0 {
|
||||
// \n -> \r\n
|
||||
substitutedEmail = bytes.Replace(*email, []byte{10}, []byte{13, 10}, -1)
|
||||
}
|
||||
|
||||
parts := bytes.SplitN(substitutedEmail, []byte{13, 10, 13, 10}, 2)
|
||||
if len(parts) != 2 {
|
||||
return []byte{}, []byte{}, ErrBadMailFormat
|
||||
}
|
||||
// Empty body
|
||||
if len(parts[1]) == 0 {
|
||||
parts[1] = []byte{13, 10}
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
545
vendor/github.com/toorop/go-dkim/dkimHeader.go
generated
vendored
545
vendor/github.com/toorop/go-dkim/dkimHeader.go
generated
vendored
@ -1,545 +0,0 @@
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DKIMHeader struct {
|
||||
// Version This tag defines the version of DKIM
|
||||
// specification that applies to the signature record.
|
||||
// tag v
|
||||
Version string
|
||||
|
||||
// The algorithm used to generate the signature..
|
||||
// Verifiers MUST support "rsa-sha1" and "rsa-sha256";
|
||||
// Signers SHOULD sign using "rsa-sha256".
|
||||
// tag a
|
||||
Algorithm string
|
||||
|
||||
// The signature data (base64).
|
||||
// Whitespace is ignored in this value and MUST be
|
||||
// ignored when reassembling the original signature.
|
||||
// In particular, the signing process can safely insert
|
||||
// FWS in this value in arbitrary places to conform to line-length
|
||||
// limits.
|
||||
// tag b
|
||||
SignatureData string
|
||||
|
||||
// The hash of the canonicalized body part of the message as
|
||||
// limited by the "l=" tag (base64; REQUIRED).
|
||||
// Whitespace is ignored in this value and MUST be ignored when reassembling the original
|
||||
// signature. In particular, the signing process can safely insert
|
||||
// FWS in this value in arbitrary places to conform to line-length
|
||||
// limits.
|
||||
// tag bh
|
||||
BodyHash string
|
||||
|
||||
// Message canonicalization (plain-text; OPTIONAL, default is
|
||||
//"simple/simple"). This tag informs the Verifier of the type of
|
||||
// canonicalization used to prepare the message for signing. It
|
||||
// consists of two names separated by a "slash" (%d47) character,
|
||||
// corresponding to the header and body canonicalization algorithms,
|
||||
// respectively. These algorithms are described in Section 3.4. If
|
||||
// only one algorithm is named, that algorithm is used for the header
|
||||
// and "simple" is used for the body. For example, "c=relaxed" is
|
||||
// treated the same as "c=relaxed/simple".
|
||||
// tag c
|
||||
MessageCanonicalization string
|
||||
|
||||
// The SDID claiming responsibility for an introduction of a message
|
||||
// into the mail stream (plain-text; REQUIRED). Hence, the SDID
|
||||
// value is used to form the query for the public key. The SDID MUST
|
||||
// correspond to a valid DNS name under which the DKIM key record is
|
||||
// published. The conventions and semantics used by a Signer to
|
||||
// create and use a specific SDID are outside the scope of this
|
||||
// specification, as is any use of those conventions and semantics.
|
||||
// When presented with a signature that does not meet these
|
||||
// requirements, Verifiers MUST consider the signature invalid.
|
||||
// Internationalized domain names MUST be encoded as A-labels, as
|
||||
// described in Section 2.3 of [RFC5890].
|
||||
// tag d
|
||||
Domain string
|
||||
|
||||
// Signed header fields (plain-text, but see description; REQUIRED).
|
||||
// A colon-separated list of header field names that identify the
|
||||
// header fields presented to the signing algorithm. The field MUST
|
||||
// contain the complete list of header fields in the order presented
|
||||
// to the signing algorithm. The field MAY contain names of header
|
||||
// fields that do not exist when signed; nonexistent header fields do
|
||||
// not contribute to the signature computation (that is, they are
|
||||
// treated as the null input, including the header field name, the
|
||||
// separating colon, the header field value, and any CRLF
|
||||
// terminator). The field MAY contain multiple instances of a header
|
||||
// field name, meaning multiple occurrences of the corresponding
|
||||
// header field are included in the header hash. The field MUST NOT
|
||||
// include the DKIM-Signature header field that is being created or
|
||||
// verified but may include others. Folding whitespace (FWS) MAY be
|
||||
// included on either side of the colon separator. Header field
|
||||
// names MUST be compared against actual header field names in a
|
||||
// case-insensitive manner. This list MUST NOT be empty. See
|
||||
// Section 5.4 for a discussion of choosing header fields to sign and
|
||||
// Section 5.4.2 for requirements when signing multiple instances of
|
||||
// a single field.
|
||||
// tag h
|
||||
Headers []string
|
||||
|
||||
// The Agent or User Identifier (AUID) on behalf of which the SDID is
|
||||
// taking responsibility (dkim-quoted-printable; OPTIONAL, default is
|
||||
// an empty local-part followed by an "@" followed by the domain from
|
||||
// the "d=" tag).
|
||||
// The syntax is a standard email address where the local-part MAY be
|
||||
// omitted. The domain part of the address MUST be the same as, or a
|
||||
// subdomain of, the value of the "d=" tag.
|
||||
// Internationalized domain names MUST be encoded as A-labels, as
|
||||
// described in Section 2.3 of [RFC5890].
|
||||
// tag i
|
||||
Auid string
|
||||
|
||||
// Body length count (plain-text unsigned decimal integer; OPTIONAL,
|
||||
// default is entire body). This tag informs the Verifier of the
|
||||
// number of octets in the body of the email after canonicalization
|
||||
// included in the cryptographic hash, starting from 0 immediately
|
||||
// following the CRLF preceding the body. This value MUST NOT be
|
||||
// larger than the actual number of octets in the canonicalized
|
||||
// message body. See further discussion in Section 8.2.
|
||||
// tag l
|
||||
BodyLength uint
|
||||
|
||||
// A colon-separated list of query methods used to retrieve the
|
||||
// public key (plain-text; OPTIONAL, default is "dns/txt"). Each
|
||||
// query method is of the form "type[/options]", where the syntax and
|
||||
// semantics of the options depend on the type and specified options.
|
||||
// If there are multiple query mechanisms listed, the choice of query
|
||||
// mechanism MUST NOT change the interpretation of the signature.
|
||||
// Implementations MUST use the recognized query mechanisms in the
|
||||
// order presented. Unrecognized query mechanisms MUST be ignored.
|
||||
// Currently, the only valid value is "dns/txt", which defines the
|
||||
// DNS TXT resource record (RR) lookup algorithm described elsewhere
|
||||
// in this document. The only option defined for the "dns" query
|
||||
// type is "txt", which MUST be included. Verifiers and Signers MUST
|
||||
// support "dns/txt".
|
||||
// tag q
|
||||
QueryMethods []string
|
||||
|
||||
// The selector subdividing the namespace for the "d=" (domain) tag
|
||||
// (plain-text; REQUIRED).
|
||||
// Internationalized selector names MUST be encoded as A-labels, as
|
||||
// described in Section 2.3 of [RFC5890].
|
||||
// tag s
|
||||
Selector string
|
||||
|
||||
// Signature Timestamp (plain-text unsigned decimal integer;
|
||||
// RECOMMENDED, default is an unknown creation time). The time that
|
||||
// this signature was created. The format is the number of seconds
|
||||
// since 00:00:00 on January 1, 1970 in the UTC time zone. The value
|
||||
// is expressed as an unsigned integer in decimal ASCII. This value
|
||||
// is not constrained to fit into a 31- or 32-bit integer.
|
||||
// Implementations SHOULD be prepared to handle values up to at least
|
||||
// 10^12 (until approximately AD 200,000; this fits into 40 bits).
|
||||
// To avoid denial-of-service attacks, implementations MAY consider
|
||||
// any value longer than 12 digits to be infinite. Leap seconds are
|
||||
// not counted. Implementations MAY ignore signatures that have a
|
||||
// timestamp in the future.
|
||||
// tag t
|
||||
SignatureTimestamp time.Time
|
||||
|
||||
// Signature Expiration (plain-text unsigned decimal integer;
|
||||
// RECOMMENDED, default is no expiration). The format is the same as
|
||||
// in the "t=" tag, represented as an absolute date, not as a time
|
||||
// delta from the signing timestamp. The value is expressed as an
|
||||
// unsigned integer in decimal ASCII, with the same constraints on
|
||||
// the value in the "t=" tag. Signatures MAY be considered invalid
|
||||
// if the verification time at the Verifier is past the expiration
|
||||
// date. The verification time should be the time that the message
|
||||
// was first received at the administrative domain of the Verifier if
|
||||
// that time is reliably available; otherwise, the current time
|
||||
// should be used. The value of the "x=" tag MUST be greater than
|
||||
// the value of the "t=" tag if both are present.
|
||||
//tag x
|
||||
SignatureExpiration time.Time
|
||||
|
||||
// Copied header fields (dkim-quoted-printable, but see description;
|
||||
// OPTIONAL, default is null). A vertical-bar-separated list of
|
||||
// selected header fields present when the message was signed,
|
||||
// including both the field name and value. It is not required to
|
||||
// include all header fields present at the time of signing. This
|
||||
// field need not contain the same header fields listed in the "h="
|
||||
// tag. The header field text itself must encode the vertical bar
|
||||
// ("|", %x7C) character (i.e., vertical bars in the "z=" text are
|
||||
// meta-characters, and any actual vertical bar characters in a
|
||||
// copied header field must be encoded). Note that all whitespace
|
||||
// must be encoded, including whitespace between the colon and the
|
||||
// header field value. After encoding, FWS MAY be added at arbitrary
|
||||
// locations in order to avoid excessively long lines; such
|
||||
// whitespace is NOT part of the value of the header field and MUST
|
||||
// be removed before decoding.
|
||||
// The header fields referenced by the "h=" tag refer to the fields
|
||||
// in the [RFC5322] header of the message, not to any copied fields
|
||||
// in the "z=" tag. Copied header field values are for diagnostic
|
||||
// use.
|
||||
// tag z
|
||||
CopiedHeaderFields []string
|
||||
|
||||
// HeaderMailFromDomain store the raw email address of the header Mail From
|
||||
// used for verifying in case of multiple DKIM header (we will prioritise
|
||||
// header with d = mail from domain)
|
||||
//HeaderMailFromDomain string
|
||||
|
||||
// RawForsign represents the raw part (without canonicalization) of the header
|
||||
// used for computint sig in verify process
|
||||
rawForSign string
|
||||
}
|
||||
|
||||
// NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value
|
||||
func newDkimHeaderBySigOptions(options SigOptions) *DKIMHeader {
|
||||
h := new(DKIMHeader)
|
||||
h.Version = "1"
|
||||
h.Algorithm = options.Algo
|
||||
h.MessageCanonicalization = options.Canonicalization
|
||||
h.Domain = options.Domain
|
||||
h.Headers = options.Headers
|
||||
h.Auid = options.Auid
|
||||
h.BodyLength = options.BodyLength
|
||||
h.QueryMethods = options.QueryMethods
|
||||
h.Selector = options.Selector
|
||||
if options.AddSignatureTimestamp {
|
||||
h.SignatureTimestamp = time.Now()
|
||||
}
|
||||
if options.SignatureExpireIn > 0 {
|
||||
h.SignatureExpiration = time.Now().Add(time.Duration(options.SignatureExpireIn) * time.Second)
|
||||
}
|
||||
h.CopiedHeaderFields = options.CopiedHeaderFields
|
||||
return h
|
||||
}
|
||||
|
||||
// GetHeader return a new DKIMHeader by parsing an email
|
||||
// Note: according to RFC 6376 an email can have multiple DKIM Header
|
||||
// in this case we return the last inserted or the last with d== mail from
|
||||
func GetHeader(email *[]byte) (*DKIMHeader, error) {
|
||||
m, err := mail.ReadMessage(bytes.NewReader(*email))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// DKIM header ?
|
||||
if len(m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")]) == 0 {
|
||||
return nil, ErrDkimHeaderNotFound
|
||||
}
|
||||
|
||||
// Get mail from domain
|
||||
mailFromDomain := ""
|
||||
mailfrom, err := mail.ParseAddress(m.Header.Get(textproto.CanonicalMIMEHeaderKey("From")))
|
||||
if err != nil {
|
||||
if err.Error() != "mail: no address" {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
t := strings.SplitAfter(mailfrom.Address, "@")
|
||||
if len(t) > 1 {
|
||||
mailFromDomain = strings.ToLower(t[1])
|
||||
}
|
||||
}
|
||||
|
||||
// get raw dkim header
|
||||
// we can't use m.header because header key will be converted with textproto.CanonicalMIMEHeaderKey
|
||||
// ie if key in header is not DKIM-Signature but Dkim-Signature or DKIM-signature ot... other
|
||||
// combination of case, verify will fail.
|
||||
rawHeaders, _, err := getHeadersBody(email)
|
||||
if err != nil {
|
||||
return nil, ErrBadMailFormat
|
||||
}
|
||||
rawHeadersList, err := getHeadersList(&rawHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dkHeaders := []string{}
|
||||
for h := rawHeadersList.Front(); h != nil; h = h.Next() {
|
||||
if strings.HasPrefix(strings.ToLower(h.Value.(string)), "dkim-signature") {
|
||||
dkHeaders = append(dkHeaders, h.Value.(string))
|
||||
}
|
||||
}
|
||||
|
||||
var keep *DKIMHeader
|
||||
var keepErr error
|
||||
//for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] {
|
||||
for _, h := range dkHeaders {
|
||||
parsed, err := parseDkHeader(h)
|
||||
// if malformed dkim header try next
|
||||
if err != nil {
|
||||
keepErr = err
|
||||
continue
|
||||
}
|
||||
// Keep first dkim headers
|
||||
if keep == nil {
|
||||
keep = parsed
|
||||
}
|
||||
// if d flag == domain keep this header and return
|
||||
if mailFromDomain == parsed.Domain {
|
||||
return parsed, nil
|
||||
}
|
||||
}
|
||||
if keep == nil {
|
||||
return nil, keepErr
|
||||
}
|
||||
return keep, nil
|
||||
}
|
||||
|
||||
// parseDkHeader parse raw dkim header
|
||||
func parseDkHeader(header string) (dkh *DKIMHeader, err error) {
|
||||
dkh = new(DKIMHeader)
|
||||
|
||||
keyVal := strings.SplitN(header, ":", 2)
|
||||
|
||||
t := strings.LastIndex(header, "b=")
|
||||
if t == -1 {
|
||||
return nil, ErrDkimHeaderBTagNotFound
|
||||
}
|
||||
dkh.rawForSign = header[0 : t+2]
|
||||
p := strings.IndexByte(header[t:], ';')
|
||||
if p != -1 {
|
||||
dkh.rawForSign = dkh.rawForSign + header[t+p:]
|
||||
}
|
||||
|
||||
// Mandatory
|
||||
mandatoryFlags := make(map[string]bool, 7) //(b'v', b'a', b'b', b'bh', b'd', b'h', b's')
|
||||
mandatoryFlags["v"] = false
|
||||
mandatoryFlags["a"] = false
|
||||
mandatoryFlags["b"] = false
|
||||
mandatoryFlags["bh"] = false
|
||||
mandatoryFlags["d"] = false
|
||||
mandatoryFlags["h"] = false
|
||||
mandatoryFlags["s"] = false
|
||||
|
||||
// default values
|
||||
dkh.MessageCanonicalization = "simple/simple"
|
||||
dkh.QueryMethods = []string{"dns/txt"}
|
||||
|
||||
// unfold && clean
|
||||
val := removeFWS(keyVal[1])
|
||||
val = strings.Replace(val, " ", "", -1)
|
||||
|
||||
fs := strings.Split(val, ";")
|
||||
for _, f := range fs {
|
||||
if f == "" {
|
||||
continue
|
||||
}
|
||||
flagData := strings.SplitN(f, "=", 2)
|
||||
|
||||
// https://github.com/toorop/go-dkim/issues/2
|
||||
// if flag is not in the form key=value (eg doesn't have "=")
|
||||
if len(flagData) != 2 {
|
||||
return nil, ErrDkimHeaderBadFormat
|
||||
}
|
||||
flag := strings.ToLower(strings.TrimSpace(flagData[0]))
|
||||
data := strings.TrimSpace(flagData[1])
|
||||
switch flag {
|
||||
case "v":
|
||||
if data != "1" {
|
||||
return nil, ErrDkimVersionNotsupported
|
||||
}
|
||||
dkh.Version = data
|
||||
mandatoryFlags["v"] = true
|
||||
case "a":
|
||||
dkh.Algorithm = strings.ToLower(data)
|
||||
if dkh.Algorithm != "rsa-sha1" && dkh.Algorithm != "rsa-sha256" {
|
||||
return nil, ErrSignBadAlgo
|
||||
}
|
||||
mandatoryFlags["a"] = true
|
||||
case "b":
|
||||
//dkh.SignatureData = removeFWS(data)
|
||||
// remove all space
|
||||
dkh.SignatureData = strings.Replace(removeFWS(data), " ", "", -1)
|
||||
if len(dkh.SignatureData) != 0 {
|
||||
mandatoryFlags["b"] = true
|
||||
}
|
||||
case "bh":
|
||||
dkh.BodyHash = removeFWS(data)
|
||||
if len(dkh.BodyHash) != 0 {
|
||||
mandatoryFlags["bh"] = true
|
||||
}
|
||||
case "d":
|
||||
dkh.Domain = strings.ToLower(data)
|
||||
if len(dkh.Domain) != 0 {
|
||||
mandatoryFlags["d"] = true
|
||||
}
|
||||
case "h":
|
||||
data = strings.ToLower(data)
|
||||
dkh.Headers = strings.Split(data, ":")
|
||||
if len(dkh.Headers) != 0 {
|
||||
mandatoryFlags["h"] = true
|
||||
}
|
||||
fromFound := false
|
||||
for _, h := range dkh.Headers {
|
||||
if h == "from" {
|
||||
fromFound = true
|
||||
}
|
||||
}
|
||||
if !fromFound {
|
||||
return nil, ErrDkimHeaderNoFromInHTag
|
||||
}
|
||||
case "s":
|
||||
dkh.Selector = strings.ToLower(data)
|
||||
if len(dkh.Selector) != 0 {
|
||||
mandatoryFlags["s"] = true
|
||||
}
|
||||
case "c":
|
||||
dkh.MessageCanonicalization, err = validateCanonicalization(strings.ToLower(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "i":
|
||||
if data != "" {
|
||||
if !strings.HasSuffix(data, dkh.Domain) {
|
||||
return nil, ErrDkimHeaderDomainMismatch
|
||||
}
|
||||
dkh.Auid = data
|
||||
}
|
||||
case "l":
|
||||
ui, err := strconv.ParseUint(data, 10, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dkh.BodyLength = uint(ui)
|
||||
case "q":
|
||||
dkh.QueryMethods = strings.Split(data, ":")
|
||||
if len(dkh.QueryMethods) == 0 || strings.ToLower(dkh.QueryMethods[0]) != "dns/txt" {
|
||||
return nil, errQueryMethodNotsupported
|
||||
}
|
||||
case "t":
|
||||
ts, err := strconv.ParseInt(data, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dkh.SignatureTimestamp = time.Unix(ts, 0)
|
||||
|
||||
case "x":
|
||||
ts, err := strconv.ParseInt(data, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dkh.SignatureExpiration = time.Unix(ts, 0)
|
||||
case "z":
|
||||
dkh.CopiedHeaderFields = strings.Split(data, "|")
|
||||
}
|
||||
}
|
||||
|
||||
// All mandatory flags are in ?
|
||||
for _, p := range mandatoryFlags {
|
||||
if !p {
|
||||
return nil, ErrDkimHeaderMissingRequiredTag
|
||||
}
|
||||
}
|
||||
|
||||
// default for i/Auid
|
||||
if dkh.Auid == "" {
|
||||
dkh.Auid = "@" + dkh.Domain
|
||||
}
|
||||
|
||||
// defaut for query method
|
||||
if len(dkh.QueryMethods) == 0 {
|
||||
dkh.QueryMethods = []string{"dns/text"}
|
||||
}
|
||||
|
||||
return dkh, nil
|
||||
|
||||
}
|
||||
|
||||
// GetHeaderBase return base header for signers
|
||||
// Todo: some refactoring needed...
|
||||
func (d *DKIMHeader) getHeaderBaseForSigning(bodyHash string) string {
|
||||
h := "DKIM-Signature: v=" + d.Version + "; a=" + d.Algorithm + "; q=" + strings.Join(d.QueryMethods, ":") + "; c=" + d.MessageCanonicalization + ";" + CRLF + TAB
|
||||
subh := "s=" + d.Selector + ";"
|
||||
if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
}
|
||||
subh += " d=" + d.Domain + ";"
|
||||
|
||||
// Auid
|
||||
if len(d.Auid) != 0 {
|
||||
if len(subh)+len(d.Auid)+4 > MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
}
|
||||
subh += " i=" + d.Auid + ";"
|
||||
}
|
||||
|
||||
/*h := "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tmail.io; i=@tmail.io;" + FWS
|
||||
subh := "q=dns/txt; s=test;"*/
|
||||
|
||||
// signature timestamp
|
||||
if !d.SignatureTimestamp.IsZero() {
|
||||
ts := d.SignatureTimestamp.Unix()
|
||||
if len(subh)+14 > MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
}
|
||||
subh += " t=" + fmt.Sprintf("%d", ts) + ";"
|
||||
}
|
||||
if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
}
|
||||
|
||||
// Expiration
|
||||
if !d.SignatureExpiration.IsZero() {
|
||||
ts := d.SignatureExpiration.Unix()
|
||||
if len(subh)+14 > MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
}
|
||||
subh += " x=" + fmt.Sprintf("%d", ts) + ";"
|
||||
}
|
||||
|
||||
// body length
|
||||
if d.BodyLength != 0 {
|
||||
bodyLengthStr := fmt.Sprintf("%d", d.BodyLength)
|
||||
if len(subh)+len(bodyLengthStr)+4 > MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
}
|
||||
subh += " l=" + bodyLengthStr + ";"
|
||||
}
|
||||
|
||||
// Headers
|
||||
if len(subh)+len(d.Headers)+4 > MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
}
|
||||
subh += " h="
|
||||
for _, header := range d.Headers {
|
||||
if len(subh)+len(header)+1 > MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
}
|
||||
subh += header + ":"
|
||||
}
|
||||
subh = subh[:len(subh)-1] + ";"
|
||||
|
||||
// BodyHash
|
||||
if len(subh)+5+len(bodyHash) > MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
} else {
|
||||
subh += " "
|
||||
}
|
||||
subh += "bh="
|
||||
l := len(subh)
|
||||
for _, c := range bodyHash {
|
||||
subh += string(c)
|
||||
l++
|
||||
if l >= MaxHeaderLineLength {
|
||||
h += subh + FWS
|
||||
subh = ""
|
||||
l = 0
|
||||
}
|
||||
}
|
||||
h += subh + ";" + FWS + "b="
|
||||
return h
|
||||
}
|
94
vendor/github.com/toorop/go-dkim/errors.go
generated
vendored
94
vendor/github.com/toorop/go-dkim/errors.go
generated
vendored
@ -1,94 +0,0 @@
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrSignPrivateKeyRequired when there not private key in config
|
||||
ErrSignPrivateKeyRequired = errors.New("PrivateKey is required")
|
||||
|
||||
// ErrSignDomainRequired when there is no domain defined in config
|
||||
ErrSignDomainRequired = errors.New("Domain is required")
|
||||
|
||||
// ErrSignSelectorRequired when there is no Selcteir defined in config
|
||||
ErrSignSelectorRequired = errors.New("Selector is required")
|
||||
|
||||
// ErrSignHeaderShouldContainsFrom If Headers is specified it should at least contain 'from'
|
||||
ErrSignHeaderShouldContainsFrom = errors.New("header must contains 'from' field")
|
||||
|
||||
// ErrSignBadCanonicalization If bad Canonicalization parameter
|
||||
ErrSignBadCanonicalization = errors.New("bad Canonicalization parameter")
|
||||
|
||||
// ErrCandNotParsePrivateKey when unable to parse private key
|
||||
ErrCandNotParsePrivateKey = errors.New("can not parse private key, check format (pem) and validity")
|
||||
|
||||
// ErrSignBadAlgo Bad algorithm
|
||||
ErrSignBadAlgo = errors.New("bad algorithm. Only rsa-sha1 or rsa-sha256 are permitted")
|
||||
|
||||
// ErrBadMailFormat unable to parse mail
|
||||
ErrBadMailFormat = errors.New("bad mail format")
|
||||
|
||||
// ErrBadMailFormatHeaders bad headers format (not DKIM Header)
|
||||
ErrBadMailFormatHeaders = errors.New("bad mail format found in headers")
|
||||
|
||||
// ErrBadDKimTagLBodyTooShort bad l tag
|
||||
ErrBadDKimTagLBodyTooShort = errors.New("bad tag l or bodyLength option. Body length < l value")
|
||||
|
||||
// ErrDkimHeaderBadFormat when errors found in DKIM header
|
||||
ErrDkimHeaderBadFormat = errors.New("bad DKIM header format")
|
||||
|
||||
// ErrDkimHeaderNotFound when there's no DKIM-Signature header in an email we have to verify
|
||||
ErrDkimHeaderNotFound = errors.New("no DKIM-Signature header field found ")
|
||||
|
||||
// ErrDkimHeaderBTagNotFound when there's no b tag
|
||||
ErrDkimHeaderBTagNotFound = errors.New("no tag 'b' found in dkim header")
|
||||
|
||||
// ErrDkimHeaderNoFromInHTag when from is missing in h tag
|
||||
ErrDkimHeaderNoFromInHTag = errors.New("'from' header is missing in h tag")
|
||||
|
||||
// ErrDkimHeaderMissingRequiredTag when a required tag is missing
|
||||
ErrDkimHeaderMissingRequiredTag = errors.New("signature missing required tag")
|
||||
|
||||
// ErrDkimHeaderDomainMismatch if i tag is not a sub domain of d tag
|
||||
ErrDkimHeaderDomainMismatch = errors.New("domain mismatch")
|
||||
|
||||
// ErrDkimVersionNotsupported version not supported
|
||||
ErrDkimVersionNotsupported = errors.New("incompatible version")
|
||||
|
||||
// Query method unsupported
|
||||
errQueryMethodNotsupported = errors.New("query method not supported")
|
||||
|
||||
// ErrVerifyBodyHash when body hash doesn't verify
|
||||
ErrVerifyBodyHash = errors.New("body hash did not verify")
|
||||
|
||||
// ErrVerifyNoKeyForSignature no key
|
||||
ErrVerifyNoKeyForSignature = errors.New("no key for verify")
|
||||
|
||||
// ErrVerifyKeyUnavailable when service (dns) is anavailable
|
||||
ErrVerifyKeyUnavailable = errors.New("key unavailable")
|
||||
|
||||
// ErrVerifyTagVMustBeTheFirst if present the v tag must be the firts in the record
|
||||
ErrVerifyTagVMustBeTheFirst = errors.New("pub key syntax error: v tag must be the first")
|
||||
|
||||
// ErrVerifyVersionMusBeDkim1 if présent flag v (version) must be DKIM1
|
||||
ErrVerifyVersionMusBeDkim1 = errors.New("flag v must be set to DKIM1")
|
||||
|
||||
// ErrVerifyBadKeyType bad type for pub key (only rsa is accepted)
|
||||
ErrVerifyBadKeyType = errors.New("bad type for key type")
|
||||
|
||||
// ErrVerifyRevokedKey key(s) for this selector is revoked (p is empty)
|
||||
ErrVerifyRevokedKey = errors.New("revoked key")
|
||||
|
||||
// ErrVerifyBadKey when we can't parse pubkey
|
||||
ErrVerifyBadKey = errors.New("unable to parse pub key")
|
||||
|
||||
// ErrVerifyNoKey when no key is found on DNS record
|
||||
ErrVerifyNoKey = errors.New("no public key found in DNS TXT")
|
||||
|
||||
// ErrVerifySignatureHasExpired when signature has expired
|
||||
ErrVerifySignatureHasExpired = errors.New("signature has expired")
|
||||
|
||||
// ErrVerifyInappropriateHashAlgo when h tag in pub key doesn't contain hash algo from a tag of DKIM header
|
||||
ErrVerifyInappropriateHashAlgo = errors.New("inappropriate has algorithm")
|
||||
)
|
181
vendor/github.com/toorop/go-dkim/pubKeyRep.go
generated
vendored
181
vendor/github.com/toorop/go-dkim/pubKeyRep.go
generated
vendored
@ -1,181 +0,0 @@
|
||||
package dkim
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"mime/quotedprintable"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PubKeyRep represents a parsed version of public key record
|
||||
type PubKeyRep struct {
|
||||
Version string
|
||||
HashAlgo []string
|
||||
KeyType string
|
||||
Note string
|
||||
PubKey rsa.PublicKey
|
||||
ServiceType []string
|
||||
FlagTesting bool // flag y
|
||||
FlagIMustBeD bool // flag i
|
||||
}
|
||||
|
||||
// DNSOptions holds settings for looking up DNS records
|
||||
type DNSOptions struct {
|
||||
netLookupTXT func(name string) ([]string, error)
|
||||
}
|
||||
|
||||
// DNSOpt represents an optional setting for looking up DNS records
|
||||
type DNSOpt interface {
|
||||
apply(*DNSOptions)
|
||||
}
|
||||
|
||||
type dnsOpt func(*DNSOptions)
|
||||
|
||||
func (opt dnsOpt) apply(dnsOpts *DNSOptions) {
|
||||
opt(dnsOpts)
|
||||
}
|
||||
|
||||
// DNSOptLookupTXT sets the function to use to lookup TXT records.
|
||||
//
|
||||
// This should probably only be used in tests.
|
||||
func DNSOptLookupTXT(netLookupTXT func(name string) ([]string, error)) DNSOpt {
|
||||
return dnsOpt(func(opts *DNSOptions) {
|
||||
opts.netLookupTXT = netLookupTXT
|
||||
})
|
||||
}
|
||||
|
||||
// NewPubKeyRespFromDNS retrieves the TXT record from DNS based on the specified domain and selector
|
||||
// and parses it.
|
||||
func NewPubKeyRespFromDNS(selector, domain string, opts ...DNSOpt) (*PubKeyRep, verifyOutput, error) {
|
||||
dnsOpts := DNSOptions{}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.apply(&dnsOpts)
|
||||
}
|
||||
|
||||
if dnsOpts.netLookupTXT == nil {
|
||||
dnsOpts.netLookupTXT = net.LookupTXT
|
||||
}
|
||||
|
||||
txt, err := dnsOpts.netLookupTXT(selector + "._domainkey." + domain)
|
||||
if err != nil {
|
||||
if strings.HasSuffix(err.Error(), "no such host") {
|
||||
return nil, PERMFAIL, ErrVerifyNoKeyForSignature
|
||||
}
|
||||
|
||||
return nil, TEMPFAIL, ErrVerifyKeyUnavailable
|
||||
}
|
||||
|
||||
// empty record
|
||||
if len(txt) == 0 {
|
||||
return nil, PERMFAIL, ErrVerifyNoKeyForSignature
|
||||
}
|
||||
|
||||
// parsing, we keep the first record
|
||||
// TODO: if there is multiple record
|
||||
|
||||
return NewPubKeyResp(txt[0])
|
||||
}
|
||||
|
||||
// NewPubKeyResp parses DKIM record (usually from DNS)
|
||||
func NewPubKeyResp(dkimRecord string) (*PubKeyRep, verifyOutput, error) {
|
||||
pkr := new(PubKeyRep)
|
||||
pkr.Version = "DKIM1"
|
||||
pkr.HashAlgo = []string{"sha1", "sha256"}
|
||||
pkr.KeyType = "rsa"
|
||||
pkr.FlagTesting = false
|
||||
pkr.FlagIMustBeD = false
|
||||
|
||||
p := strings.Split(dkimRecord, ";")
|
||||
for i, data := range p {
|
||||
keyVal := strings.SplitN(data, "=", 2)
|
||||
val := ""
|
||||
if len(keyVal) > 1 {
|
||||
val = strings.TrimSpace(keyVal[1])
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(keyVal[0])) {
|
||||
case "v":
|
||||
// RFC: is this tag is specified it MUST be the first in the record
|
||||
if i != 0 {
|
||||
return nil, PERMFAIL, ErrVerifyTagVMustBeTheFirst
|
||||
}
|
||||
pkr.Version = val
|
||||
if pkr.Version != "DKIM1" {
|
||||
return nil, PERMFAIL, ErrVerifyVersionMusBeDkim1
|
||||
}
|
||||
case "h":
|
||||
p := strings.Split(strings.ToLower(val), ":")
|
||||
pkr.HashAlgo = []string{}
|
||||
for _, h := range p {
|
||||
h = strings.TrimSpace(h)
|
||||
if h == "sha1" || h == "sha256" {
|
||||
pkr.HashAlgo = append(pkr.HashAlgo, h)
|
||||
}
|
||||
}
|
||||
// if empty switch back to default
|
||||
if len(pkr.HashAlgo) == 0 {
|
||||
pkr.HashAlgo = []string{"sha1", "sha256"}
|
||||
}
|
||||
case "k":
|
||||
if strings.ToLower(val) != "rsa" {
|
||||
return nil, PERMFAIL, ErrVerifyBadKeyType
|
||||
}
|
||||
case "n":
|
||||
qp, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(val)))
|
||||
if err == nil {
|
||||
val = string(qp)
|
||||
}
|
||||
pkr.Note = val
|
||||
case "p":
|
||||
rawkey := val
|
||||
if rawkey == "" {
|
||||
return nil, PERMFAIL, ErrVerifyRevokedKey
|
||||
}
|
||||
un64, err := base64.StdEncoding.DecodeString(rawkey)
|
||||
if err != nil {
|
||||
return nil, PERMFAIL, ErrVerifyBadKey
|
||||
}
|
||||
pk, err := x509.ParsePKIXPublicKey(un64)
|
||||
if pk, ok := pk.(*rsa.PublicKey); ok {
|
||||
pkr.PubKey = *pk
|
||||
}
|
||||
case "s":
|
||||
t := strings.Split(strings.ToLower(val), ":")
|
||||
for _, tt := range t {
|
||||
tt = strings.TrimSpace(tt)
|
||||
switch tt {
|
||||
case "*":
|
||||
pkr.ServiceType = append(pkr.ServiceType, "all")
|
||||
case "email":
|
||||
pkr.ServiceType = append(pkr.ServiceType, tt)
|
||||
}
|
||||
}
|
||||
case "t":
|
||||
flags := strings.Split(strings.ToLower(val), ":")
|
||||
for _, flag := range flags {
|
||||
flag = strings.TrimSpace(flag)
|
||||
switch flag {
|
||||
case "y":
|
||||
pkr.FlagTesting = true
|
||||
case "s":
|
||||
pkr.FlagIMustBeD = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no pubkey
|
||||
if pkr.PubKey == (rsa.PublicKey{}) {
|
||||
return nil, PERMFAIL, ErrVerifyNoKey
|
||||
}
|
||||
|
||||
// No service type
|
||||
if len(pkr.ServiceType) == 0 {
|
||||
pkr.ServiceType = []string{"all"}
|
||||
}
|
||||
|
||||
return pkr, SUCCESS, nil
|
||||
}
|
4
vendor/github.com/toorop/go-dkim/watch
generated
vendored
4
vendor/github.com/toorop/go-dkim/watch
generated
vendored
@ -1,4 +0,0 @@
|
||||
while true
|
||||
do
|
||||
inotifywait -q -r -e modify,attrib,close_write,move,create,delete . && echo "--------------" && go test -v
|
||||
done
|
69
vendor/golang.org/x/crypto/ed25519/ed25519.go
generated
vendored
Normal file
69
vendor/golang.org/x/crypto/ed25519/ed25519.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package ed25519 implements the Ed25519 signature algorithm. See
|
||||
// https://ed25519.cr.yp.to/.
|
||||
//
|
||||
// These functions are also compatible with the “Ed25519” function defined in
|
||||
// RFC 8032. However, unlike RFC 8032's formulation, this package's private key
|
||||
// representation includes a public key suffix to make multiple signing
|
||||
// operations with the same key more efficient. This package refers to the RFC
|
||||
// 8032 private key as the “seed”.
|
||||
//
|
||||
// This package is a wrapper around the standard library crypto/ed25519 package.
|
||||
package ed25519
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
// PublicKeySize is the size, in bytes, of public keys as used in this package.
|
||||
PublicKeySize = 32
|
||||
// PrivateKeySize is the size, in bytes, of private keys as used in this package.
|
||||
PrivateKeySize = 64
|
||||
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
|
||||
SignatureSize = 64
|
||||
// SeedSize is the size, in bytes, of private key seeds. These are the private key representations used by RFC 8032.
|
||||
SeedSize = 32
|
||||
)
|
||||
|
||||
// PublicKey is the type of Ed25519 public keys.
|
||||
//
|
||||
// This type is an alias for crypto/ed25519's PublicKey type.
|
||||
// See the crypto/ed25519 package for the methods on this type.
|
||||
type PublicKey = ed25519.PublicKey
|
||||
|
||||
// PrivateKey is the type of Ed25519 private keys. It implements crypto.Signer.
|
||||
//
|
||||
// This type is an alias for crypto/ed25519's PrivateKey type.
|
||||
// See the crypto/ed25519 package for the methods on this type.
|
||||
type PrivateKey = ed25519.PrivateKey
|
||||
|
||||
// GenerateKey generates a public/private key pair using entropy from rand.
|
||||
// If rand is nil, crypto/rand.Reader will be used.
|
||||
func GenerateKey(rand io.Reader) (PublicKey, PrivateKey, error) {
|
||||
return ed25519.GenerateKey(rand)
|
||||
}
|
||||
|
||||
// NewKeyFromSeed calculates a private key from a seed. It will panic if
|
||||
// len(seed) is not SeedSize. This function is provided for interoperability
|
||||
// with RFC 8032. RFC 8032's private keys correspond to seeds in this
|
||||
// package.
|
||||
func NewKeyFromSeed(seed []byte) PrivateKey {
|
||||
return ed25519.NewKeyFromSeed(seed)
|
||||
}
|
||||
|
||||
// Sign signs the message with privateKey and returns a signature. It will
|
||||
// panic if len(privateKey) is not PrivateKeySize.
|
||||
func Sign(privateKey PrivateKey, message []byte) []byte {
|
||||
return ed25519.Sign(privateKey, message)
|
||||
}
|
||||
|
||||
// Verify reports whether sig is a valid signature of message by publicKey. It
|
||||
// will panic if len(publicKey) is not PublicKeySize.
|
||||
func Verify(publicKey PublicKey, message, sig []byte) bool {
|
||||
return ed25519.Verify(publicKey, message, sig)
|
||||
}
|
5
vendor/modules.txt
vendored
5
vendor/modules.txt
vendored
@ -10,6 +10,9 @@ github.com/GehirnInc/crypt/md5_crypt
|
||||
# github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||
## 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user