浏览代码

fix #920, #921

tags/v2.1.0-rc1
Shivaram Lingamneni 4 年前
父节点
当前提交
ee05a4324d

+ 1
- 0
Makefile 查看文件

@@ -22,6 +22,7 @@ test:
22 22
 	cd irc/caps && go test . && go vet .
23 23
 	cd irc/cloaks && go test . && go vet .
24 24
 	cd irc/connection_limits && go test . && go vet .
25
+	cd irc/email && go test . && go vet .
25 26
 	cd irc/history && go test . && go vet .
26 27
 	cd irc/isupport && go test . && go vet .
27 28
 	cd irc/modes && go test . && go vet .

+ 15
- 7
conventional.yaml 查看文件

@@ -280,16 +280,24 @@ accounts:
280 280
         enabled-callbacks:
281 281
             - none # no verification needed, will instantly register successfully
282 282
 
283
-        # example configuration for sending verification emails via a local mail relay
283
+        # example configuration for sending verification emails
284 284
         # callbacks:
285 285
         #     mailto:
286
-        #         server: localhost
287
-        #         port: 25
288
-        #         tls:
289
-        #             enabled: false
290
-        #         username: ""
291
-        #         password: ""
292 286
         #         sender: "admin@my.network"
287
+        #         require-tls: true
288
+        #         helo-domain: "my.network" # defaults to server name if unset
289
+        #         dkim:
290
+        #             domain: "my.network"
291
+        #             selector: "20200229"
292
+        #             key-file: "dkim.pem"
293
+        #         # to use an MTA/smarthost instead of sending email directly:
294
+        #         # mta:
295
+        #         #     server: localhost
296
+        #         #     port: 25
297
+        #         #     username: "admin"
298
+        #         #     password: "hunter2"
299
+        #         blacklist-regexes:
300
+        #         #    - ".*@mailinator.com"
293 301
 
294 302
     # throttle account login attempts (to prevent either password guessing, or DoS
295 303
     # attacks on the server aimed at forcing repeated expensive bcrypt computations)

+ 1
- 0
go.mod 查看文件

@@ -18,6 +18,7 @@ require (
18 18
 	github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
19 19
 	github.com/stretchr/testify v1.4.0 // indirect
20 20
 	github.com/tidwall/buntdb v1.1.2
21
+	github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb
21 22
 	golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
22 23
 	golang.org/x/text v0.3.2
23 24
 	gopkg.in/yaml.v2 v2.2.8

+ 2
- 0
go.sum 查看文件

@@ -65,6 +65,8 @@ github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2K
65 65
 github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
66 66
 github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
67 67
 github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
68
+github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb h1:ilDZC+k9r67aJqSOalZLtEVLO7Cmmsq5ftfcvLirc24=
69
+github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
68 70
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
69 71
 golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4=
70 72
 golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

+ 25
- 31
irc/accounts.go 查看文件

@@ -4,9 +4,9 @@
4 4
 package irc
5 5
 
6 6
 import (
7
+	"bytes"
7 8
 	"encoding/json"
8 9
 	"fmt"
9
-	"net/smtp"
10 10
 	"strconv"
11 11
 	"strings"
12 12
 	"sync"
@@ -16,6 +16,7 @@ import (
16 16
 
17 17
 	"github.com/oragono/oragono/irc/caps"
18 18
 	"github.com/oragono/oragono/irc/connection_limits"
19
+	"github.com/oragono/oragono/irc/email"
19 20
 	"github.com/oragono/oragono/irc/ldap"
20 21
 	"github.com/oragono/oragono/irc/passwd"
21 22
 	"github.com/oragono/oragono/irc/utils"
@@ -483,7 +484,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
483 484
 		return err
484 485
 	}
485 486
 
486
-	code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue)
487
+	code, err := am.dispatchCallback(client, account, callbackNamespace, callbackValue)
487 488
 	if err != nil {
488 489
 		am.Unregister(casefoldedAccount, true)
489 490
 		return errCallbackFailed
@@ -698,17 +699,17 @@ func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasP
698 699
 	return err
699 700
 }
700 701
 
701
-func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
702
+func (am *AccountManager) dispatchCallback(client *Client, account string, callbackNamespace string, callbackValue string) (string, error) {
702 703
 	if callbackNamespace == "*" || callbackNamespace == "none" || callbackNamespace == "admin" {
703 704
 		return "", nil
704 705
 	} else if callbackNamespace == "mailto" {
705
-		return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue)
706
+		return am.dispatchMailtoCallback(client, account, callbackValue)
706 707
 	} else {
707 708
 		return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace)
708 709
 	}
709 710
 }
710 711
 
711
-func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
712
+func (am *AccountManager) dispatchMailtoCallback(client *Client, account string, callbackValue string) (code string, err error) {
712 713
 	config := am.server.Config().Accounts.Registration.Callbacks.Mailto
713 714
 	code = utils.GenerateSecretToken()
714 715
 
@@ -716,34 +717,27 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou
716 717
 	if subject == "" {
717 718
 		subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
718 719
 	}
719
-	messageStrings := []string{
720
-		fmt.Sprintf("From: %s\r\n", config.Sender),
721
-		fmt.Sprintf("To: %s\r\n", callbackValue),
722
-		fmt.Sprintf("Subject: %s\r\n", subject),
723
-		"\r\n", // end headers, begin message body
724
-		fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n",
725
-		fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
726
-		"\r\n",
727
-		client.t("To verify your account, issue the following command:") + "\r\n",
728
-		fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
729
-	}
730
-
731
-	var message []byte
732
-	for i := 0; i < len(messageStrings); i++ {
733
-		message = append(message, []byte(messageStrings[i])...)
734
-	}
735
-	addr := fmt.Sprintf("%s:%d", config.Server, config.Port)
736
-	var auth smtp.Auth
737
-	if config.Username != "" && config.Password != "" {
738
-		auth = smtp.PlainAuth("", config.Username, config.Password, config.Server)
739
-	}
740 720
 
741
-	// TODO: this will never send the password in plaintext over a nonlocal link,
742
-	// but it might send the email in plaintext, regardless of the value of
743
-	// config.TLS.InsecureSkipVerify
744
-	err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
721
+	var message bytes.Buffer
722
+	fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
723
+	fmt.Fprintf(&message, "To: %s\r\n", callbackValue)
724
+	if config.DKIM.Domain != "" {
725
+		fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), config.DKIM.Domain)
726
+	}
727
+	fmt.Fprintf(&message, "Subject: %s\r\n", subject)
728
+	message.WriteString("\r\n") // blank line: end headers, begin message body
729
+	fmt.Fprintf(&message, client.t("Account: %s"), account)
730
+	message.WriteString("\r\n")
731
+	fmt.Fprintf(&message, client.t("Verification code: %s"), code)
732
+	message.WriteString("\r\n")
733
+	message.WriteString("\r\n")
734
+	message.WriteString(client.t("To verify your account, issue the following command:"))
735
+	message.WriteString("\r\n")
736
+	fmt.Fprintf(&message, "/MSG NickServ VERIFY %s %s\r\n", account, code)
737
+
738
+	err = email.SendMail(config, callbackValue, message.Bytes())
745 739
 	if err != nil {
746
-		am.server.logger.Error("internal", "Failed to dispatch e-mail", err.Error())
740
+		am.server.logger.Error("internal", "Failed to dispatch e-mail to", callbackValue, err.Error())
747 741
 	}
748 742
 	return
749 743
 }

+ 12
- 14
irc/config.go 查看文件

@@ -24,6 +24,7 @@ import (
24 24
 	"github.com/oragono/oragono/irc/cloaks"
25 25
 	"github.com/oragono/oragono/irc/connection_limits"
26 26
 	"github.com/oragono/oragono/irc/custime"
27
+	"github.com/oragono/oragono/irc/email"
27 28
 	"github.com/oragono/oragono/irc/isupport"
28 29
 	"github.com/oragono/oragono/irc/languages"
29 30
 	"github.com/oragono/oragono/irc/ldap"
@@ -290,20 +291,7 @@ type AccountRegistrationConfig struct {
290 291
 	EnabledCredentialTypes []string         `yaml:"-"`
291 292
 	VerifyTimeout          custime.Duration `yaml:"verify-timeout"`
292 293
 	Callbacks              struct {
293
-		Mailto struct {
294
-			Server string
295
-			Port   int
296
-			TLS    struct {
297
-				Enabled            bool
298
-				InsecureSkipVerify bool   `yaml:"insecure_skip_verify"`
299
-				ServerName         string `yaml:"servername"`
300
-			}
301
-			Username             string
302
-			Password             string
303
-			Sender               string
304
-			VerifyMessageSubject string `yaml:"verify-message-subject"`
305
-			VerifyMessage        string `yaml:"verify-message"`
306
-		}
294
+		Mailto email.MailtoConfig
307 295
 	}
308 296
 	BcryptCost uint `yaml:"bcrypt-cost"`
309 297
 }
@@ -975,14 +963,24 @@ func LoadConfig(filename string) (config *Config, err error) {
975 963
 
976 964
 	// hardcode this for now
977 965
 	config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"}
966
+	mailtoEnabled := false
978 967
 	for i, name := range config.Accounts.Registration.EnabledCallbacks {
979 968
 		if name == "none" {
980 969
 			// we store "none" as "*" internally
981 970
 			config.Accounts.Registration.EnabledCallbacks[i] = "*"
971
+		} else if name == "mailto" {
972
+			mailtoEnabled = true
982 973
 		}
983 974
 	}
984 975
 	sort.Strings(config.Accounts.Registration.EnabledCallbacks)
985 976
 
977
+	if mailtoEnabled {
978
+		err := config.Accounts.Registration.Callbacks.Mailto.Postprocess(config.Server.Name)
979
+		if err != nil {
980
+			return nil, err
981
+		}
982
+	}
983
+
986 984
 	config.Accounts.RequireSasl.exemptedNets, err = utils.ParseNetList(config.Accounts.RequireSasl.Exempted)
987 985
 	if err != nil {
988 986
 		return nil, fmt.Errorf("Could not parse require-sasl exempted nets: %v", err.Error())

+ 54
- 0
irc/email/dkim.go 查看文件

@@ -0,0 +1,54 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package email
5
+
6
+import (
7
+	"errors"
8
+	dkim "github.com/toorop/go-dkim"
9
+	"io/ioutil"
10
+)
11
+
12
+var (
13
+	ErrMissingFields = errors.New("DKIM config is missing fields")
14
+)
15
+
16
+type DKIMConfig struct {
17
+	Domain   string
18
+	Selector string
19
+	KeyFile  string `yaml:"key-file"`
20
+	keyBytes []byte
21
+}
22
+
23
+func (dkim *DKIMConfig) Postprocess() (err error) {
24
+	if dkim.Domain != "" {
25
+		if dkim.Selector == "" || dkim.KeyFile == "" {
26
+			return ErrMissingFields
27
+		}
28
+		dkim.keyBytes, err = ioutil.ReadFile(dkim.KeyFile)
29
+		if err != nil {
30
+			return err
31
+		}
32
+	}
33
+	return nil
34
+}
35
+
36
+var defaultOptions = dkim.SigOptions{
37
+	Version:               1,
38
+	Canonicalization:      "relaxed/relaxed",
39
+	Algo:                  "rsa-sha256",
40
+	Headers:               []string{"from", "to", "subject", "message-id"},
41
+	BodyLength:            0,
42
+	QueryMethods:          []string{"dns/txt"},
43
+	AddSignatureTimestamp: true,
44
+	SignatureExpireIn:     0,
45
+}
46
+
47
+func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) {
48
+	options := defaultOptions
49
+	options.PrivateKey = dkimConfig.keyBytes
50
+	options.Domain = dkimConfig.Domain
51
+	options.Selector = dkimConfig.Selector
52
+	err = dkim.Sign(&message, options)
53
+	return message, err
54
+}

+ 124
- 0
irc/email/email.go 查看文件

@@ -0,0 +1,124 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package email
5
+
6
+import (
7
+	"errors"
8
+	"fmt"
9
+	"net"
10
+	"regexp"
11
+	"strings"
12
+
13
+	"github.com/oragono/oragono/irc/smtp"
14
+)
15
+
16
+var (
17
+	ErrBlacklistedAddress = errors.New("Email address is blacklisted")
18
+	ErrInvalidAddress     = errors.New("Email address is blacklisted")
19
+	ErrNoMXRecord         = errors.New("Couldn't resolve MX record")
20
+)
21
+
22
+type MTAConfig struct {
23
+	Server   string
24
+	Port     int
25
+	Username string
26
+	Password string
27
+}
28
+
29
+type MailtoConfig struct {
30
+	// legacy config format assumed the use of an MTA/smarthost,
31
+	// so server, port, etc. appear directly at top level
32
+	// XXX: see https://github.com/go-yaml/yaml/issues/63
33
+	MTAConfig            `yaml:",inline"`
34
+	Sender               string
35
+	HeloDomain           string `yaml:"helo-domain"`
36
+	RequireTLS           bool   `yaml:"require-tls"`
37
+	VerifyMessageSubject string `yaml:"verify-message-subject"`
38
+	DKIM                 DKIMConfig
39
+	MTAReal              MTAConfig `yaml:"mta"`
40
+	BlacklistRegexes     []string  `yaml:"blacklist-regexes"`
41
+	blacklistRegexes     []*regexp.Regexp
42
+}
43
+
44
+func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
45
+	if config.Sender == "" {
46
+		return errors.New("Invalid mailto sender address")
47
+	}
48
+
49
+	// check for MTA config fields at top level,
50
+	// copy to MTAReal if present
51
+	if config.Server != "" && config.MTAReal.Server == "" {
52
+		config.MTAReal = config.MTAConfig
53
+	}
54
+
55
+	if config.HeloDomain == "" {
56
+		config.HeloDomain = heloDomain
57
+	}
58
+
59
+	for _, reg := range config.BlacklistRegexes {
60
+		compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg))
61
+		if err != nil {
62
+			return err
63
+		}
64
+		config.blacklistRegexes = append(config.blacklistRegexes, compiled)
65
+	}
66
+
67
+	if config.MTAConfig.Server != "" {
68
+		// smarthost, nothing more to validate
69
+		return nil
70
+	}
71
+
72
+	return config.DKIM.Postprocess()
73
+}
74
+
75
+// get the preferred MX record hostname, "" on error
76
+func lookupMX(domain string) (server string) {
77
+	var minPref uint16
78
+	results, err := net.LookupMX(domain)
79
+	if err != nil {
80
+		return
81
+	}
82
+	for _, result := range results {
83
+		if minPref == 0 || result.Pref < minPref {
84
+			server, minPref = result.Host, result.Pref
85
+		}
86
+	}
87
+	return
88
+}
89
+
90
+func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
91
+	for _, reg := range config.blacklistRegexes {
92
+		if reg.MatchString(recipient) {
93
+			return ErrBlacklistedAddress
94
+		}
95
+	}
96
+
97
+	if config.DKIM.Domain != "" {
98
+		msg, err = DKIMSign(msg, config.DKIM)
99
+		if err != nil {
100
+			return
101
+		}
102
+	}
103
+
104
+	var addr string
105
+	var auth smtp.Auth
106
+	if config.MTAReal.Server != "" {
107
+		addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
108
+		if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
109
+			auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
110
+		}
111
+	} else {
112
+		idx := strings.IndexByte(recipient, '@')
113
+		if idx == -1 {
114
+			return ErrInvalidAddress
115
+		}
116
+		mx := lookupMX(recipient[idx+1:])
117
+		if mx == "" {
118
+			return ErrNoMXRecord
119
+		}
120
+		addr = fmt.Sprintf("%s:smtp", mx)
121
+	}
122
+
123
+	return smtp.SendMail(addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg, config.RequireTLS)
124
+}

+ 5
- 2
irc/smtp/smtp.go 查看文件

@@ -316,7 +316,8 @@ var testHookStartTLS func(*tls.Config) // nil, except for tests
316 316
 // attachments (see the mime/multipart package), or other mail
317 317
 // functionality. Higher-level packages exist outside of the standard
318 318
 // library.
319
-func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
319
+// XXX: modified in Oragono to add `requireTLS` and `heloDomain` arguments
320
+func SendMail(addr string, a Auth, heloDomain string, from string, to []string, msg []byte, requireTLS bool) error {
320 321
 	if err := validateLine(from); err != nil {
321 322
 		return err
322 323
 	}
@@ -330,7 +331,7 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
330 331
 		return err
331 332
 	}
332 333
 	defer c.Close()
333
-	if err = c.hello(); err != nil {
334
+	if err = c.Hello(heloDomain); err != nil {
334 335
 		return err
335 336
 	}
336 337
 	if ok, _ := c.Extension("STARTTLS"); ok {
@@ -341,6 +342,8 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
341 342
 		if err = c.StartTLS(config); err != nil {
342 343
 			return err
343 344
 		}
345
+	} else if requireTLS {
346
+		return errors.New("TLS required, but not negotiated")
344 347
 	}
345 348
 	if a != nil && c.ext != nil {
346 349
 		if _, ok := c.ext["AUTH"]; !ok {

+ 15
- 7
oragono.yaml 查看文件

@@ -301,16 +301,24 @@ accounts:
301 301
         enabled-callbacks:
302 302
             - none # no verification needed, will instantly register successfully
303 303
 
304
-        # example configuration for sending verification emails via a local mail relay
304
+        # example configuration for sending verification emails
305 305
         # callbacks:
306 306
         #     mailto:
307
-        #         server: localhost
308
-        #         port: 25
309
-        #         tls:
310
-        #             enabled: false
311
-        #         username: ""
312
-        #         password: ""
313 307
         #         sender: "admin@my.network"
308
+        #         require-tls: true
309
+        #         helo-domain: "my.network" # defaults to server name if unset
310
+        #         dkim:
311
+        #             domain: "my.network"
312
+        #             selector: "20200229"
313
+        #             key-file: "dkim.pem"
314
+        #         # to use an MTA/smarthost instead of sending email directly:
315
+        #         # mta:
316
+        #         #     server: localhost
317
+        #         #     port: 25
318
+        #         #     username: "admin"
319
+        #         #     password: "hunter2"
320
+        #         blacklist-regexes:
321
+        #         #    - ".*@mailinator.com"
314 322
 
315 323
     # throttle account login attempts (to prevent either password guessing, or DoS
316 324
     # attacks on the server aimed at forcing repeated expensive bcrypt computations)

+ 24
- 0
vendor/github.com/toorop/go-dkim/.gitignore 查看文件

@@ -0,0 +1,24 @@
1
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
2
+*.o
3
+*.a
4
+*.so
5
+
6
+# Folders
7
+_obj
8
+_test
9
+
10
+# Architecture specific extensions/prefixes
11
+*.[568vq]
12
+[568vq].out
13
+
14
+*.cgo1.go
15
+*.cgo2.c
16
+_cgo_defun.c
17
+_cgo_gotypes.go
18
+_cgo_export.*
19
+
20
+_testmain.go
21
+
22
+*.exe
23
+*.test
24
+*.prof

+ 22
- 0
vendor/github.com/toorop/go-dkim/LICENSE 查看文件

@@ -0,0 +1,22 @@
1
+The MIT License (MIT)
2
+
3
+Copyright (c) 2015 Stéphane Depierrepont
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.
22
+

+ 56
- 0
vendor/github.com/toorop/go-dkim/README.md 查看文件

@@ -0,0 +1,56 @@
1
+# go-dkim
2
+DKIM package for Golang
3
+
4
+[![GoDoc](https://godoc.org/github.com/toorop/go-dkim?status.svg)](https://godoc.org/github.com/toorop/go-dkim)
5
+
6
+## Getting started
7
+
8
+### Install
9
+```
10
+ 	go get github.com/toorop/go-dkim
11
+```
12
+Warning: you need to use Go 1.4.2-master or 1.4.3 (when it will be available)
13
+see https://github.com/golang/go/issues/10482 fro more info.
14
+
15
+### Sign email
16
+
17
+```go
18
+import (
19
+	dkim "github.com/toorop/go-dkim"
20
+)
21
+
22
+func main(){
23
+	// email is the email to sign (byte slice)
24
+	// privateKey the private key (pem encoded, byte slice )	
25
+	options := dkim.NewSigOptions()
26
+	options.PrivateKey = privateKey
27
+	options.Domain = "mydomain.tld"
28
+	options.Selector = "myselector"
29
+	options.SignatureExpireIn = 3600
30
+	options.BodyLength = 50
31
+	options.Headers = []string{"from", "date", "mime-version", "received", "received"}
32
+	options.AddSignatureTimestamp = true
33
+	options.Canonicalization = "relaxed/relaxed"
34
+	err := dkim.Sign(&email, options)
35
+	// handle err..
36
+
37
+	// And... that's it, 'email' is signed ! Amazing© !!!
38
+}
39
+```
40
+
41
+### Verify
42
+```go
43
+import (
44
+	dkim "github.com/toorop/go-dkim"
45
+)
46
+
47
+func main(){
48
+	// email is the email to verify (byte slice)
49
+	status, err := Verify(&email)
50
+	// handle status, err (see godoc for status)
51
+}
52
+```
53
+
54
+## Todo
55
+
56
+- [ ] handle z tag (copied header fields used for diagnostic use)

+ 557
- 0
vendor/github.com/toorop/go-dkim/dkim.go 查看文件

@@ -0,0 +1,557 @@
1
+// Package dkim provides tools for signing and verify a email according to RFC 6376
2
+package dkim
3
+
4
+import (
5
+	"bytes"
6
+	"container/list"
7
+	"crypto"
8
+	"crypto/rand"
9
+	"crypto/rsa"
10
+	"crypto/sha1"
11
+	"crypto/sha256"
12
+	"crypto/x509"
13
+	"encoding/base64"
14
+	"encoding/pem"
15
+	"hash"
16
+	"regexp"
17
+	"strings"
18
+	"time"
19
+)
20
+
21
+const (
22
+	CRLF                = "\r\n"
23
+	TAB                 = " "
24
+	FWS                 = CRLF + TAB
25
+	MaxHeaderLineLength = 70
26
+)
27
+
28
+type verifyOutput int
29
+
30
+const (
31
+	SUCCESS verifyOutput = 1 + iota
32
+	PERMFAIL
33
+	TEMPFAIL
34
+	NOTSIGNED
35
+	TESTINGSUCCESS
36
+	TESTINGPERMFAIL
37
+	TESTINGTEMPFAIL
38
+)
39
+
40
+// sigOptions represents signing options
41
+type SigOptions struct {
42
+
43
+	// DKIM version (default 1)
44
+	Version uint
45
+
46
+	// Private key used for signing (required)
47
+	PrivateKey []byte
48
+
49
+	// Domain (required)
50
+	Domain string
51
+
52
+	// Selector (required)
53
+	Selector string
54
+
55
+	// The Agent of User IDentifier
56
+	Auid string
57
+
58
+	// Message canonicalization (plain-text; OPTIONAL, default is
59
+	// "simple/simple").  This tag informs the Verifier of the type of
60
+	// canonicalization used to prepare the message for signing.
61
+	Canonicalization string
62
+
63
+	// The algorithm used to generate the signature
64
+	//"rsa-sha1" or "rsa-sha256"
65
+	Algo string
66
+
67
+	// Signed header fields
68
+	Headers []string
69
+
70
+	// Body length count( if set to 0 this tag is ommited in Dkim header)
71
+	BodyLength uint
72
+
73
+	// Query Methods used to retrieve the public key
74
+	QueryMethods []string
75
+
76
+	// Add a signature timestamp
77
+	AddSignatureTimestamp bool
78
+
79
+	// Time validity of the signature (0=never)
80
+	SignatureExpireIn uint64
81
+
82
+	// CopiedHeaderFileds
83
+	CopiedHeaderFields []string
84
+}
85
+
86
+// NewSigOptions returns new sigoption with some defaults value
87
+func NewSigOptions() SigOptions {
88
+	return SigOptions{
89
+		Version:               1,
90
+		Canonicalization:      "simple/simple",
91
+		Algo:                  "rsa-sha256",
92
+		Headers:               []string{"from"},
93
+		BodyLength:            0,
94
+		QueryMethods:          []string{"dns/txt"},
95
+		AddSignatureTimestamp: true,
96
+		SignatureExpireIn:     0,
97
+	}
98
+}
99
+
100
+// Sign signs an email
101
+func Sign(email *[]byte, options SigOptions) error {
102
+	var privateKey *rsa.PrivateKey
103
+
104
+	// PrivateKey
105
+	if len(options.PrivateKey) == 0 {
106
+		return ErrSignPrivateKeyRequired
107
+	}
108
+	d, _ := pem.Decode(options.PrivateKey)
109
+	if d == nil {
110
+		return ErrCandNotParsePrivateKey
111
+	}
112
+	key, err := x509.ParsePKCS1PrivateKey(d.Bytes)
113
+	if err != nil {
114
+		return ErrCandNotParsePrivateKey
115
+	}
116
+	privateKey = key
117
+
118
+	// Domain required
119
+	if options.Domain == "" {
120
+		return ErrSignDomainRequired
121
+	}
122
+
123
+	// Selector required
124
+	if options.Selector == "" {
125
+		return ErrSignSelectorRequired
126
+	}
127
+
128
+	// Canonicalization
129
+	options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization))
130
+	if err != nil {
131
+		return err
132
+	}
133
+
134
+	// Algo
135
+	options.Algo = strings.ToLower(options.Algo)
136
+	if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" {
137
+		return ErrSignBadAlgo
138
+	}
139
+
140
+	// Header must contain "from"
141
+	hasFrom := false
142
+	for i, h := range options.Headers {
143
+		h = strings.ToLower(h)
144
+		options.Headers[i] = h
145
+		if h == "from" {
146
+			hasFrom = true
147
+		}
148
+	}
149
+	if !hasFrom {
150
+		return ErrSignHeaderShouldContainsFrom
151
+	}
152
+
153
+	// Normalize
154
+	headers, body, err := canonicalize(email, options.Canonicalization, options.Headers)
155
+	if err != nil {
156
+		return err
157
+	}
158
+
159
+	signHash := strings.Split(options.Algo, "-")
160
+
161
+	// hash body
162
+	bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength)
163
+	if err != nil {
164
+		return err
165
+	}
166
+
167
+	// Get dkim header base
168
+	dkimHeader := newDkimHeaderBySigOptions(options)
169
+	dHeader := dkimHeader.getHeaderBaseForSigning(bodyHash)
170
+
171
+	canonicalizations := strings.Split(options.Canonicalization, "/")
172
+	dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0])
173
+	if err != nil {
174
+		return err
175
+	}
176
+	headers = append(headers, []byte(dHeaderCanonicalized)...)
177
+	headers = bytes.TrimRight(headers, " \r\n")
178
+
179
+	// sign
180
+	sig, err := getSignature(&headers, privateKey, signHash[1])
181
+
182
+	// add to DKIM-Header
183
+	subh := ""
184
+	l := len(subh)
185
+	for _, c := range sig {
186
+		subh += string(c)
187
+		l++
188
+		if l >= MaxHeaderLineLength {
189
+			dHeader += subh + FWS
190
+			subh = ""
191
+			l = 0
192
+		}
193
+	}
194
+	dHeader += subh + CRLF
195
+	*email = append([]byte(dHeader), *email...)
196
+	return nil
197
+}
198
+
199
+// Verify verifies an email an return
200
+// state: SUCCESS or PERMFAIL or TEMPFAIL, TESTINGSUCCESS, TESTINGPERMFAIL
201
+// TESTINGTEMPFAIL or NOTSIGNED
202
+// error: if an error occurs during verification
203
+func Verify(email *[]byte, opts ...DNSOpt) (verifyOutput, error) {
204
+	// parse email
205
+	dkimHeader, err := newDkimHeaderFromEmail(email)
206
+	if err != nil {
207
+		if err == ErrDkimHeaderNotFound {
208
+			return NOTSIGNED, ErrDkimHeaderNotFound
209
+		}
210
+		return PERMFAIL, err
211
+	}
212
+
213
+	// we do not set query method because if it's others, validation failed earlier
214
+	pubKey, verifyOutputOnError, err := NewPubKeyRespFromDNS(dkimHeader.Selector, dkimHeader.Domain, opts...)
215
+	if err != nil {
216
+		// fix https://github.com/toorop/go-dkim/issues/1
217
+		//return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting)
218
+		return verifyOutputOnError, err
219
+	}
220
+
221
+	// Normalize
222
+	headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers)
223
+	if err != nil {
224
+		return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting)
225
+	}
226
+	sigHash := strings.Split(dkimHeader.Algorithm, "-")
227
+	// check if hash algo are compatible
228
+	compatible := false
229
+	for _, algo := range pubKey.HashAlgo {
230
+		if sigHash[1] == algo {
231
+			compatible = true
232
+			break
233
+		}
234
+	}
235
+	if !compatible {
236
+		return getVerifyOutput(PERMFAIL, ErrVerifyInappropriateHashAlgo, pubKey.FlagTesting)
237
+	}
238
+
239
+	// expired ?
240
+	if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() {
241
+		return getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting)
242
+
243
+	}
244
+
245
+	//println("|" + string(body) + "|")
246
+	// get body hash
247
+	bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength)
248
+	if err != nil {
249
+		return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting)
250
+	}
251
+	//println(bodyHash)
252
+	if bodyHash != dkimHeader.BodyHash {
253
+		return getVerifyOutput(PERMFAIL, ErrVerifyBodyHash, pubKey.FlagTesting)
254
+	}
255
+
256
+	// compute sig
257
+	dkimHeaderCano, err := canonicalizeHeader(dkimHeader.RawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0])
258
+	if err != nil {
259
+		return getVerifyOutput(TEMPFAIL, err, pubKey.FlagTesting)
260
+	}
261
+	toSignStr := string(headers) + dkimHeaderCano
262
+	toSign := bytes.TrimRight([]byte(toSignStr), " \r\n")
263
+
264
+	err = verifySignature(toSign, dkimHeader.SignatureData, &pubKey.PubKey, sigHash[1])
265
+	if err != nil {
266
+		return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting)
267
+	}
268
+	return SUCCESS, nil
269
+}
270
+
271
+// getVerifyOutput returns output of verify fct according to the testing flag
272
+func getVerifyOutput(status verifyOutput, err error, flagTesting bool) (verifyOutput, error) {
273
+	if !flagTesting {
274
+		return status, err
275
+	}
276
+	switch status {
277
+	case SUCCESS:
278
+		return TESTINGSUCCESS, err
279
+	case PERMFAIL:
280
+		return TESTINGPERMFAIL, err
281
+	case TEMPFAIL:
282
+		return TESTINGTEMPFAIL, err
283
+	}
284
+	// should never happen but compilator sream whithout return
285
+	return status, err
286
+}
287
+
288
+// canonicalize returns canonicalized version of header and body
289
+func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, err error) {
290
+	body = []byte{}
291
+	rxReduceWS := regexp.MustCompile(`[ \t]+`)
292
+
293
+	rawHeaders, rawBody, err := getHeadersBody(email)
294
+	if err != nil {
295
+		return nil, nil, err
296
+	}
297
+
298
+	canonicalizations := strings.Split(cano, "/")
299
+
300
+	// canonicalyze header
301
+	headersList, err := getHeadersList(&rawHeaders)
302
+
303
+	// pour chaque header a conserver on traverse tous les headers dispo
304
+	// If multi instance of a field we must keep it from the bottom to the top
305
+	var match *list.Element
306
+	headersToKeepList := list.New()
307
+
308
+	for _, headerToKeep := range h {
309
+		match = nil
310
+		headerToKeepToLower := strings.ToLower(headerToKeep)
311
+		for e := headersList.Front(); e != nil; e = e.Next() {
312
+			//fmt.Printf("|%s|\n", e.Value.(string))
313
+			t := strings.Split(e.Value.(string), ":")
314
+			if strings.ToLower(t[0]) == headerToKeepToLower {
315
+				match = e
316
+			}
317
+		}
318
+		if match != nil {
319
+			headersToKeepList.PushBack(match.Value.(string) + "\r\n")
320
+			headersList.Remove(match)
321
+		}
322
+	}
323
+
324
+	//if canonicalizations[0] == "simple" {
325
+	for e := headersToKeepList.Front(); e != nil; e = e.Next() {
326
+		cHeader, err := canonicalizeHeader(e.Value.(string), canonicalizations[0])
327
+		if err != nil {
328
+			return headers, body, err
329
+		}
330
+		headers = append(headers, []byte(cHeader)...)
331
+	}
332
+	// canonicalyze body
333
+	if canonicalizations[1] == "simple" {
334
+		// simple
335
+		// The "simple" body canonicalization algorithm ignores all empty lines
336
+		// at the end of the message body.  An empty line is a line of zero
337
+		// length after removal of the line terminator.  If there is no body or
338
+		// no trailing CRLF on the message body, a CRLF is added.  It makes no
339
+		// other changes to the message body.  In more formal terms, the
340
+		// "simple" body canonicalization algorithm converts "*CRLF" at the end
341
+		// of the body to a single "CRLF".
342
+		// Note that a completely empty or missing body is canonicalized as a
343
+		// single "CRLF"; that is, the canonicalized length will be 2 octets.
344
+		body = bytes.TrimRight(rawBody, "\r\n")
345
+		body = append(body, []byte{13, 10}...)
346
+	} else {
347
+		// relaxed
348
+		// Ignore all whitespace at the end of lines.  Implementations
349
+		// MUST NOT remove the CRLF at the end of the line.
350
+		// Reduce all sequences of WSP within a line to a single SP
351
+		// character.
352
+		// Ignore all empty lines at the end of the message body.  "Empty
353
+		// line" is defined in Section 3.4.3.  If the body is non-empty but
354
+		// does not end with a CRLF, a CRLF is added.  (For email, this is
355
+		// only possible when using extensions to SMTP or non-SMTP transport
356
+		// mechanisms.)
357
+		rawBody = rxReduceWS.ReplaceAll(rawBody, []byte(" "))
358
+		for _, line := range bytes.SplitAfter(rawBody, []byte{10}) {
359
+			line = bytes.TrimRight(line, " \r\n")
360
+			body = append(body, line...)
361
+			body = append(body, []byte{13, 10}...)
362
+		}
363
+		body = bytes.TrimRight(body, "\r\n")
364
+		body = append(body, []byte{13, 10}...)
365
+
366
+	}
367
+	return
368
+}
369
+
370
+// canonicalizeHeader returns canonicalized version of header
371
+func canonicalizeHeader(header string, algo string) (string, error) {
372
+	//rxReduceWS := regexp.MustCompile(`[ \t]+`)
373
+	if algo == "simple" {
374
+		// The "simple" header canonicalization algorithm does not change header
375
+		// fields in any way.  Header fields MUST be presented to the signing or
376
+		// verification algorithm exactly as they are in the message being
377
+		// signed or verified.  In particular, header field names MUST NOT be
378
+		// case folded and whitespace MUST NOT be changed.
379
+		return header, nil
380
+	} else if algo == "relaxed" {
381
+		// The "relaxed" header canonicalization algorithm MUST apply the
382
+		// following steps in order:
383
+
384
+		// Convert all header field names (not the header field values) to
385
+		// lowercase.  For example, convert "SUBJect: AbC" to "subject: AbC".
386
+
387
+		// Unfold all header field continuation lines as described in
388
+		// [RFC5322]; in particular, lines with terminators embedded in
389
+		// continued header field values (that is, CRLF sequences followed by
390
+		// WSP) MUST be interpreted without the CRLF.  Implementations MUST
391
+		// NOT remove the CRLF at the end of the header field value.
392
+
393
+		// Convert all sequences of one or more WSP characters to a single SP
394
+		// character.  WSP characters here include those before and after a
395
+		// line folding boundary.
396
+
397
+		// Delete all WSP characters at the end of each unfolded header field
398
+		// value.
399
+
400
+		// Delete any WSP characters remaining before and after the colon
401
+		// separating the header field name from the header field value.  The
402
+		// colon separator MUST be retained.
403
+		kv := strings.SplitN(header, ":", 2)
404
+		if len(kv) != 2 {
405
+			return header, ErrBadMailFormatHeaders
406
+		}
407
+		k := strings.ToLower(kv[0])
408
+		k = strings.TrimSpace(k)
409
+		v := removeFWS(kv[1])
410
+		//v = rxReduceWS.ReplaceAllString(v, " ")
411
+		//v = strings.TrimSpace(v)
412
+		return k + ":" + v + CRLF, nil
413
+	}
414
+	return header, ErrSignBadCanonicalization
415
+}
416
+
417
+// getBodyHash return the hash (bas64encoded) of the body
418
+func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) {
419
+	var h hash.Hash
420
+	if algo == "sha1" {
421
+		h = sha1.New()
422
+	} else {
423
+		h = sha256.New()
424
+	}
425
+	toH := *body
426
+	// if l tag (body length)
427
+	if bodyLength != 0 {
428
+		if uint(len(toH)) < bodyLength {
429
+			return "", ErrBadDKimTagLBodyTooShort
430
+		}
431
+		toH = toH[0:bodyLength]
432
+	}
433
+
434
+	h.Write(toH)
435
+	return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
436
+}
437
+
438
+// getSignature return signature of toSign using key
439
+func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, error) {
440
+	var h1 hash.Hash
441
+	var h2 crypto.Hash
442
+	switch algo {
443
+	case "sha1":
444
+		h1 = sha1.New()
445
+		h2 = crypto.SHA1
446
+		break
447
+	case "sha256":
448
+		h1 = sha256.New()
449
+		h2 = crypto.SHA256
450
+		break
451
+	default:
452
+		return "", ErrVerifyInappropriateHashAlgo
453
+	}
454
+
455
+	// sign
456
+	h1.Write(*toSign)
457
+	sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil))
458
+	if err != nil {
459
+		return "", err
460
+	}
461
+	return base64.StdEncoding.EncodeToString(sig), nil
462
+}
463
+
464
+// verifySignature verify signature from pubkey
465
+func verifySignature(toSign []byte, sig64 string, key *rsa.PublicKey, algo string) error {
466
+	var h1 hash.Hash
467
+	var h2 crypto.Hash
468
+	switch algo {
469
+	case "sha1":
470
+		h1 = sha1.New()
471
+		h2 = crypto.SHA1
472
+		break
473
+	case "sha256":
474
+		h1 = sha256.New()
475
+		h2 = crypto.SHA256
476
+		break
477
+	default:
478
+		return ErrVerifyInappropriateHashAlgo
479
+	}
480
+
481
+	h1.Write(toSign)
482
+	sig, err := base64.StdEncoding.DecodeString(sig64)
483
+	if err != nil {
484
+		return err
485
+	}
486
+	return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig)
487
+}
488
+
489
+// removeFWS removes all FWS from string
490
+func removeFWS(in string) string {
491
+	rxReduceWS := regexp.MustCompile(`[ \t]+`)
492
+	out := strings.Replace(in, "\n", "", -1)
493
+	out = strings.Replace(out, "\r", "", -1)
494
+	out = rxReduceWS.ReplaceAllString(out, " ")
495
+	return strings.TrimSpace(out)
496
+}
497
+
498
+// validateCanonicalization validate canonicalization (c flag)
499
+func validateCanonicalization(cano string) (string, error) {
500
+	p := strings.Split(cano, "/")
501
+	if len(p) > 2 {
502
+		return "", ErrSignBadCanonicalization
503
+	}
504
+	if len(p) == 1 {
505
+		cano = cano + "/simple"
506
+	}
507
+	for _, c := range p {
508
+		if c != "simple" && c != "relaxed" {
509
+			return "", ErrSignBadCanonicalization
510
+		}
511
+	}
512
+	return cano, nil
513
+}
514
+
515
+// getHeadersList returns headers as list
516
+func getHeadersList(rawHeader *[]byte) (*list.List, error) {
517
+	headersList := list.New()
518
+	currentHeader := []byte{}
519
+	for _, line := range bytes.SplitAfter(*rawHeader, []byte{10}) {
520
+		if line[0] == 32 || line[0] == 9 {
521
+			if len(currentHeader) == 0 {
522
+				return headersList, ErrBadMailFormatHeaders
523
+			}
524
+			currentHeader = append(currentHeader, line...)
525
+		} else {
526
+			// New header, save current if exists
527
+			if len(currentHeader) != 0 {
528
+				headersList.PushBack(string(bytes.TrimRight(currentHeader, "\r\n")))
529
+				currentHeader = []byte{}
530
+			}
531
+			currentHeader = append(currentHeader, line...)
532
+		}
533
+	}
534
+	headersList.PushBack(string(currentHeader))
535
+	return headersList, nil
536
+}
537
+
538
+// getHeadersBody return headers and body
539
+func getHeadersBody(email *[]byte) ([]byte, []byte, error) {
540
+	substitutedEmail := *email
541
+
542
+	// only replace \n with \r\n when \r\n\r\n not exists
543
+	if bytes.Index(*email, []byte{13, 10, 13, 10}) < 0 {
544
+		// \n -> \r\n
545
+		substitutedEmail = bytes.Replace(*email, []byte{10}, []byte{13, 10}, -1)
546
+	}
547
+
548
+	parts := bytes.SplitN(substitutedEmail, []byte{13, 10, 13, 10}, 2)
549
+	if len(parts) != 2 {
550
+		return []byte{}, []byte{}, ErrBadMailFormat
551
+	}
552
+	// Empty body
553
+	if len(parts[1]) == 0 {
554
+		parts[1] = []byte{13, 10}
555
+	}
556
+	return parts[0], parts[1], nil
557
+}

+ 545
- 0
vendor/github.com/toorop/go-dkim/dkimHeader.go 查看文件

@@ -0,0 +1,545 @@
1
+package dkim
2
+
3
+import (
4
+	"bytes"
5
+	"fmt"
6
+	"net/mail"
7
+	"net/textproto"
8
+	"strconv"
9
+	"strings"
10
+	"time"
11
+)
12
+
13
+type dkimHeader struct {
14
+	// Version  This tag defines the version of DKIM
15
+	// specification that applies to the signature record.
16
+	// tag v
17
+	Version string
18
+
19
+	// The algorithm used to generate the signature..
20
+	// Verifiers MUST support "rsa-sha1" and "rsa-sha256";
21
+	// Signers SHOULD sign using "rsa-sha256".
22
+	// tag a
23
+	Algorithm string
24
+
25
+	// The signature data (base64).
26
+	// Whitespace is ignored in this value and MUST be
27
+	// ignored when reassembling the original signature.
28
+	// In particular, the signing process can safely insert
29
+	// FWS in this value in arbitrary places to conform to line-length
30
+	// limits.
31
+	// tag b
32
+	SignatureData string
33
+
34
+	// The hash of the canonicalized body part of the message as
35
+	// limited by the "l=" tag (base64; REQUIRED).
36
+	// Whitespace is ignored in this value and MUST be ignored when reassembling the original
37
+	// signature.  In particular, the signing process can safely insert
38
+	// FWS in this value in arbitrary places to conform to line-length
39
+	// limits.
40
+	// tag bh
41
+	BodyHash string
42
+
43
+	// Message canonicalization (plain-text; OPTIONAL, default is
44
+	//"simple/simple").  This tag informs the Verifier of the type of
45
+	// canonicalization used to prepare the message for signing.  It
46
+	// consists of two names separated by a "slash" (%d47) character,
47
+	// corresponding to the header and body canonicalization algorithms,
48
+	// respectively.  These algorithms are described in Section 3.4.  If
49
+	// only one algorithm is named, that algorithm is used for the header
50
+	// and "simple" is used for the body.  For example, "c=relaxed" is
51
+	// treated the same as "c=relaxed/simple".
52
+	// tag c
53
+	MessageCanonicalization string
54
+
55
+	// The SDID claiming responsibility for an introduction of a message
56
+	//  into the mail stream (plain-text; REQUIRED).  Hence, the SDID
57
+	//  value is used to form the query for the public key.  The SDID MUST
58
+	// correspond to a valid DNS name under which the DKIM key record is
59
+	// published.  The conventions and semantics used by a Signer to
60
+	// create and use a specific SDID are outside the scope of this
61
+	// specification, as is any use of those conventions and semantics.
62
+	// When presented with a signature that does not meet these
63
+	// requirements, Verifiers MUST consider the signature invalid.
64
+	// Internationalized domain names MUST be encoded as A-labels, as
65
+	// described in Section 2.3 of [RFC5890].
66
+	// tag d
67
+	Domain string
68
+
69
+	// Signed header fields (plain-text, but see description; REQUIRED).
70
+	// A colon-separated list of header field names that identify the
71
+	// header fields presented to the signing algorithm.  The field MUST
72
+	// contain the complete list of header fields in the order presented
73
+	// to the signing algorithm.  The field MAY contain names of header
74
+	// fields that do not exist when signed; nonexistent header fields do
75
+	// not contribute to the signature computation (that is, they are
76
+	// treated as the null input, including the header field name, the
77
+	// separating colon, the header field value, and any CRLF
78
+	// terminator).  The field MAY contain multiple instances of a header
79
+	// field name, meaning multiple occurrences of the corresponding
80
+	// header field are included in the header hash.  The field MUST NOT
81
+	// include the DKIM-Signature header field that is being created or
82
+	// verified but may include others.  Folding whitespace (FWS) MAY be
83
+	// included on either side of the colon separator.  Header field
84
+	// names MUST be compared against actual header field names in a
85
+	// case-insensitive manner.  This list MUST NOT be empty.  See
86
+	// Section 5.4 for a discussion of choosing header fields to sign and
87
+	// Section 5.4.2 for requirements when signing multiple instances of
88
+	// a single field.
89
+	// tag h
90
+	Headers []string
91
+
92
+	// The Agent or User Identifier (AUID) on behalf of which the SDID is
93
+	// taking responsibility (dkim-quoted-printable; OPTIONAL, default is
94
+	// an empty local-part followed by an "@" followed by the domain from
95
+	// the "d=" tag).
96
+	// The syntax is a standard email address where the local-part MAY be
97
+	// omitted.  The domain part of the address MUST be the same as, or a
98
+	// subdomain of, the value of the "d=" tag.
99
+	// Internationalized domain names MUST be encoded as A-labels, as
100
+	// described in Section 2.3 of [RFC5890].
101
+	// tag i
102
+	Auid string
103
+
104
+	// Body length count (plain-text unsigned decimal integer; OPTIONAL,
105
+	// default is entire body).  This tag informs the Verifier of the
106
+	// number of octets in the body of the email after canonicalization
107
+	// included in the cryptographic hash, starting from 0 immediately
108
+	// following the CRLF preceding the body.  This value MUST NOT be
109
+	// larger than the actual number of octets in the canonicalized
110
+	// message body.  See further discussion in Section 8.2.
111
+	// tag l
112
+	BodyLength uint
113
+
114
+	// A colon-separated list of query methods used to retrieve the
115
+	// public key (plain-text; OPTIONAL, default is "dns/txt").  Each
116
+	// query method is of the form "type[/options]", where the syntax and
117
+	// semantics of the options depend on the type and specified options.
118
+	// If there are multiple query mechanisms listed, the choice of query
119
+	// mechanism MUST NOT change the interpretation of the signature.
120
+	// Implementations MUST use the recognized query mechanisms in the
121
+	// order presented.  Unrecognized query mechanisms MUST be ignored.
122
+	// Currently, the only valid value is "dns/txt", which defines the
123
+	// DNS TXT resource record (RR) lookup algorithm described elsewhere
124
+	// in this document.  The only option defined for the "dns" query
125
+	// type is "txt", which MUST be included.  Verifiers and Signers MUST
126
+	// support "dns/txt".
127
+	// tag q
128
+	QueryMethods []string
129
+
130
+	// The selector subdividing the namespace for the "d=" (domain) tag
131
+	// (plain-text; REQUIRED).
132
+	// Internationalized selector names MUST be encoded as A-labels, as
133
+	// described in Section 2.3 of [RFC5890].
134
+	// tag s
135
+	Selector string
136
+
137
+	// Signature Timestamp (plain-text unsigned decimal integer;
138
+	// RECOMMENDED, default is an unknown creation time).  The time that
139
+	// this signature was created.  The format is the number of seconds
140
+	// since 00:00:00 on January 1, 1970 in the UTC time zone.  The value
141
+	// is expressed as an unsigned integer in decimal ASCII.  This value
142
+	// is not constrained to fit into a 31- or 32-bit integer.
143
+	// Implementations SHOULD be prepared to handle values up to at least
144
+	// 10^12 (until approximately AD 200,000; this fits into 40 bits).
145
+	// To avoid denial-of-service attacks, implementations MAY consider
146
+	// any value longer than 12 digits to be infinite.  Leap seconds are
147
+	// not counted.  Implementations MAY ignore signatures that have a
148
+	// timestamp in the future.
149
+	// tag t
150
+	SignatureTimestamp time.Time
151
+
152
+	// Signature Expiration (plain-text unsigned decimal integer;
153
+	// RECOMMENDED, default is no expiration).  The format is the same as
154
+	// in the "t=" tag, represented as an absolute date, not as a time
155
+	// delta from the signing timestamp.  The value is expressed as an
156
+	// unsigned integer in decimal ASCII, with the same constraints on
157
+	// the value in the "t=" tag.  Signatures MAY be considered invalid
158
+	// if the verification time at the Verifier is past the expiration
159
+	// date.  The verification time should be the time that the message
160
+	// was first received at the administrative domain of the Verifier if
161
+	// that time is reliably available; otherwise, the current time
162
+	// should be used.  The value of the "x=" tag MUST be greater than
163
+	// the value of the "t=" tag if both are present.
164
+	//tag x
165
+	SignatureExpiration time.Time
166
+
167
+	// Copied header fields (dkim-quoted-printable, but see description;
168
+	// OPTIONAL, default is null).  A vertical-bar-separated list of
169
+	// selected header fields present when the message was signed,
170
+	// including both the field name and value.  It is not required to
171
+	// include all header fields present at the time of signing.  This
172
+	// field need not contain the same header fields listed in the "h="
173
+	// tag.  The header field text itself must encode the vertical bar
174
+	// ("|", %x7C) character (i.e., vertical bars in the "z=" text are
175
+	// meta-characters, and any actual vertical bar characters in a
176
+	// copied header field must be encoded).  Note that all whitespace
177
+	// must be encoded, including whitespace between the colon and the
178
+	// header field value.  After encoding, FWS MAY be added at arbitrary
179
+	// locations in order to avoid excessively long lines; such
180
+	// whitespace is NOT part of the value of the header field and MUST
181
+	// be removed before decoding.
182
+	// The header fields referenced by the "h=" tag refer to the fields
183
+	// in the [RFC5322] header of the message, not to any copied fields
184
+	// in the "z=" tag.  Copied header field values are for diagnostic
185
+	// use.
186
+	// tag z
187
+	CopiedHeaderFields []string
188
+
189
+	// HeaderMailFromDomain store the raw email address of the header Mail From
190
+	// used for verifying in case of multiple DKIM header (we will prioritise
191
+	// header with d = mail from domain)
192
+	//HeaderMailFromDomain string
193
+
194
+	// RawForsign represents the raw part (without canonicalization) of the header
195
+	// used for computint sig in verify process
196
+	RawForSign string
197
+}
198
+
199
+// NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value
200
+func newDkimHeaderBySigOptions(options SigOptions) *dkimHeader {
201
+	h := new(dkimHeader)
202
+	h.Version = "1"
203
+	h.Algorithm = options.Algo
204
+	h.MessageCanonicalization = options.Canonicalization
205
+	h.Domain = options.Domain
206
+	h.Headers = options.Headers
207
+	h.Auid = options.Auid
208
+	h.BodyLength = options.BodyLength
209
+	h.QueryMethods = options.QueryMethods
210
+	h.Selector = options.Selector
211
+	if options.AddSignatureTimestamp {
212
+		h.SignatureTimestamp = time.Now()
213
+	}
214
+	if options.SignatureExpireIn > 0 {
215
+		h.SignatureExpiration = time.Now().Add(time.Duration(options.SignatureExpireIn) * time.Second)
216
+	}
217
+	h.CopiedHeaderFields = options.CopiedHeaderFields
218
+	return h
219
+}
220
+
221
+// NewFromEmail return a new DkimHeader by parsing an email
222
+// Note: according to RFC 6376 an email can have multiple DKIM Header
223
+// in this case we return the last inserted or the last with d== mail from
224
+func newDkimHeaderFromEmail(email *[]byte) (*dkimHeader, error) {
225
+	m, err := mail.ReadMessage(bytes.NewReader(*email))
226
+	if err != nil {
227
+		return nil, err
228
+	}
229
+
230
+	// DKIM header ?
231
+	if len(m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")]) == 0 {
232
+		return nil, ErrDkimHeaderNotFound
233
+	}
234
+
235
+	// Get mail from domain
236
+	mailFromDomain := ""
237
+	mailfrom, err := mail.ParseAddress(m.Header.Get(textproto.CanonicalMIMEHeaderKey("From")))
238
+	if err != nil {
239
+		if err.Error() != "mail: no address" {
240
+			return nil, err
241
+		}
242
+	} else {
243
+		t := strings.SplitAfter(mailfrom.Address, "@")
244
+		if len(t) > 1 {
245
+			mailFromDomain = strings.ToLower(t[1])
246
+		}
247
+	}
248
+
249
+	// get raw dkim header
250
+	// we can't use m.header because header key will be converted with textproto.CanonicalMIMEHeaderKey
251
+	// ie if key in header is not DKIM-Signature but Dkim-Signature or DKIM-signature ot... other
252
+	// combination of case, verify will fail.
253
+	rawHeaders, _, err := getHeadersBody(email)
254
+	if err != nil {
255
+		return nil, ErrBadMailFormat
256
+	}
257
+	rawHeadersList, err := getHeadersList(&rawHeaders)
258
+	if err != nil {
259
+		return nil, err
260
+	}
261
+	dkHeaders := []string{}
262
+	for h := rawHeadersList.Front(); h != nil; h = h.Next() {
263
+		if strings.HasPrefix(strings.ToLower(h.Value.(string)), "dkim-signature") {
264
+			dkHeaders = append(dkHeaders, h.Value.(string))
265
+		}
266
+	}
267
+
268
+	var keep *dkimHeader
269
+	var keepErr error
270
+	//for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] {
271
+	for _, h := range dkHeaders {
272
+		parsed, err := parseDkHeader(h)
273
+		// if malformed dkim header try next
274
+		if err != nil {
275
+			keepErr = err
276
+			continue
277
+		}
278
+		// Keep first dkim headers
279
+		if keep == nil {
280
+			keep = parsed
281
+		}
282
+		// if d flag == domain keep this header and return
283
+		if mailFromDomain == parsed.Domain {
284
+			return parsed, nil
285
+		}
286
+	}
287
+	if keep == nil {
288
+		return nil, keepErr
289
+	}
290
+	return keep, nil
291
+}
292
+
293
+// parseDkHeader parse raw dkim header
294
+func parseDkHeader(header string) (dkh *dkimHeader, err error) {
295
+	dkh = new(dkimHeader)
296
+
297
+	keyVal := strings.SplitN(header, ":", 2)
298
+
299
+	t := strings.LastIndex(header, "b=")
300
+	if t == -1 {
301
+		return nil, ErrDkimHeaderBTagNotFound
302
+	}
303
+	dkh.RawForSign = header[0 : t+2]
304
+	p := strings.IndexByte(header[t:], ';')
305
+	if p != -1 {
306
+		dkh.RawForSign = dkh.RawForSign + header[t+p:]
307
+	}
308
+
309
+	// Mandatory
310
+	mandatoryFlags := make(map[string]bool, 7) //(b'v', b'a', b'b', b'bh', b'd', b'h', b's')
311
+	mandatoryFlags["v"] = false
312
+	mandatoryFlags["a"] = false
313
+	mandatoryFlags["b"] = false
314
+	mandatoryFlags["bh"] = false
315
+	mandatoryFlags["d"] = false
316
+	mandatoryFlags["h"] = false
317
+	mandatoryFlags["s"] = false
318
+
319
+	// default values
320
+	dkh.MessageCanonicalization = "simple/simple"
321
+	dkh.QueryMethods = []string{"dns/txt"}
322
+
323
+	// unfold && clean
324
+	val := removeFWS(keyVal[1])
325
+	val = strings.Replace(val, " ", "", -1)
326
+
327
+	fs := strings.Split(val, ";")
328
+	for _, f := range fs {
329
+		if f == "" {
330
+			continue
331
+		}
332
+		flagData := strings.SplitN(f, "=", 2)
333
+
334
+		// https://github.com/toorop/go-dkim/issues/2
335
+		// if flag is not in the form key=value (eg doesn't have "=")
336
+		if len(flagData) != 2 {
337
+			return nil, ErrDkimHeaderBadFormat
338
+		}
339
+		flag := strings.ToLower(strings.TrimSpace(flagData[0]))
340
+		data := strings.TrimSpace(flagData[1])
341
+		switch flag {
342
+		case "v":
343
+			if data != "1" {
344
+				return nil, ErrDkimVersionNotsupported
345
+			}
346
+			dkh.Version = data
347
+			mandatoryFlags["v"] = true
348
+		case "a":
349
+			dkh.Algorithm = strings.ToLower(data)
350
+			if dkh.Algorithm != "rsa-sha1" && dkh.Algorithm != "rsa-sha256" {
351
+				return nil, ErrSignBadAlgo
352
+			}
353
+			mandatoryFlags["a"] = true
354
+		case "b":
355
+			//dkh.SignatureData = removeFWS(data)
356
+			// remove all space
357
+			dkh.SignatureData = strings.Replace(removeFWS(data), " ", "", -1)
358
+			if len(dkh.SignatureData) != 0 {
359
+				mandatoryFlags["b"] = true
360
+			}
361
+		case "bh":
362
+			dkh.BodyHash = removeFWS(data)
363
+			if len(dkh.BodyHash) != 0 {
364
+				mandatoryFlags["bh"] = true
365
+			}
366
+		case "d":
367
+			dkh.Domain = strings.ToLower(data)
368
+			if len(dkh.Domain) != 0 {
369
+				mandatoryFlags["d"] = true
370
+			}
371
+		case "h":
372
+			data = strings.ToLower(data)
373
+			dkh.Headers = strings.Split(data, ":")
374
+			if len(dkh.Headers) != 0 {
375
+				mandatoryFlags["h"] = true
376
+			}
377
+			fromFound := false
378
+			for _, h := range dkh.Headers {
379
+				if h == "from" {
380
+					fromFound = true
381
+				}
382
+			}
383
+			if !fromFound {
384
+				return nil, ErrDkimHeaderNoFromInHTag
385
+			}
386
+		case "s":
387
+			dkh.Selector = strings.ToLower(data)
388
+			if len(dkh.Selector) != 0 {
389
+				mandatoryFlags["s"] = true
390
+			}
391
+		case "c":
392
+			dkh.MessageCanonicalization, err = validateCanonicalization(strings.ToLower(data))
393
+			if err != nil {
394
+				return nil, err
395
+			}
396
+		case "i":
397
+			if data != "" {
398
+				if !strings.HasSuffix(data, dkh.Domain) {
399
+					return nil, ErrDkimHeaderDomainMismatch
400
+				}
401
+				dkh.Auid = data
402
+			}
403
+		case "l":
404
+			ui, err := strconv.ParseUint(data, 10, 32)
405
+			if err != nil {
406
+				return nil, err
407
+			}
408
+			dkh.BodyLength = uint(ui)
409
+		case "q":
410
+			dkh.QueryMethods = strings.Split(data, ":")
411
+			if len(dkh.QueryMethods) == 0 || strings.ToLower(dkh.QueryMethods[0]) != "dns/txt" {
412
+				return nil, errQueryMethodNotsupported
413
+			}
414
+		case "t":
415
+			ts, err := strconv.ParseInt(data, 10, 64)
416
+			if err != nil {
417
+				return nil, err
418
+			}
419
+			dkh.SignatureTimestamp = time.Unix(ts, 0)
420
+
421
+		case "x":
422
+			ts, err := strconv.ParseInt(data, 10, 64)
423
+			if err != nil {
424
+				return nil, err
425
+			}
426
+			dkh.SignatureExpiration = time.Unix(ts, 0)
427
+		case "z":
428
+			dkh.CopiedHeaderFields = strings.Split(data, "|")
429
+		}
430
+	}
431
+
432
+	// All mandatory flags are in ?
433
+	for _, p := range mandatoryFlags {
434
+		if !p {
435
+			return nil, ErrDkimHeaderMissingRequiredTag
436
+		}
437
+	}
438
+
439
+	// default for i/Auid
440
+	if dkh.Auid == "" {
441
+		dkh.Auid = "@" + dkh.Domain
442
+	}
443
+
444
+	// defaut for query method
445
+	if len(dkh.QueryMethods) == 0 {
446
+		dkh.QueryMethods = []string{"dns/text"}
447
+	}
448
+
449
+	return dkh, nil
450
+
451
+}
452
+
453
+// GetHeaderBase return base header for signers
454
+// Todo: some refactoring needed...
455
+func (d *dkimHeader) getHeaderBaseForSigning(bodyHash string) string {
456
+	h := "DKIM-Signature: v=" + d.Version + "; a=" + d.Algorithm + "; q=" + strings.Join(d.QueryMethods, ":") + "; c=" + d.MessageCanonicalization + ";" + CRLF + TAB
457
+	subh := "s=" + d.Selector + ";"
458
+	if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength {
459
+		h += subh + FWS
460
+		subh = ""
461
+	}
462
+	subh += " d=" + d.Domain + ";"
463
+
464
+	// Auid
465
+	if len(d.Auid) != 0 {
466
+		if len(subh)+len(d.Auid)+4 > MaxHeaderLineLength {
467
+			h += subh + FWS
468
+			subh = ""
469
+		}
470
+		subh += " i=" + d.Auid + ";"
471
+	}
472
+
473
+	/*h := "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tmail.io; i=@tmail.io;" + FWS
474
+	subh := "q=dns/txt; s=test;"*/
475
+
476
+	// signature timestamp
477
+	if !d.SignatureTimestamp.IsZero() {
478
+		ts := d.SignatureTimestamp.Unix()
479
+		if len(subh)+14 > MaxHeaderLineLength {
480
+			h += subh + FWS
481
+			subh = ""
482
+		}
483
+		subh += " t=" + fmt.Sprintf("%d", ts) + ";"
484
+	}
485
+	if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength {
486
+		h += subh + FWS
487
+		subh = ""
488
+	}
489
+
490
+	// Expiration
491
+	if !d.SignatureExpiration.IsZero() {
492
+		ts := d.SignatureExpiration.Unix()
493
+		if len(subh)+14 > MaxHeaderLineLength {
494
+			h += subh + FWS
495
+			subh = ""
496
+		}
497
+		subh += " x=" + fmt.Sprintf("%d", ts) + ";"
498
+	}
499
+
500
+	// body length
501
+	if d.BodyLength != 0 {
502
+		bodyLengthStr := fmt.Sprintf("%d", d.BodyLength)
503
+		if len(subh)+len(bodyLengthStr)+4 > MaxHeaderLineLength {
504
+			h += subh + FWS
505
+			subh = ""
506
+		}
507
+		subh += " l=" + bodyLengthStr + ";"
508
+	}
509
+
510
+	// Headers
511
+	if len(subh)+len(d.Headers)+4 > MaxHeaderLineLength {
512
+		h += subh + FWS
513
+		subh = ""
514
+	}
515
+	subh += " h="
516
+	for _, header := range d.Headers {
517
+		if len(subh)+len(header)+1 > MaxHeaderLineLength {
518
+			h += subh + FWS
519
+			subh = ""
520
+		}
521
+		subh += header + ":"
522
+	}
523
+	subh = subh[:len(subh)-1] + ";"
524
+
525
+	// BodyHash
526
+	if len(subh)+5+len(bodyHash) > MaxHeaderLineLength {
527
+		h += subh + FWS
528
+		subh = ""
529
+	} else {
530
+		subh += " "
531
+	}
532
+	subh += "bh="
533
+	l := len(subh)
534
+	for _, c := range bodyHash {
535
+		subh += string(c)
536
+		l++
537
+		if l >= MaxHeaderLineLength {
538
+			h += subh + FWS
539
+			subh = ""
540
+			l = 0
541
+		}
542
+	}
543
+	h += subh + ";" + FWS + "b="
544
+	return h
545
+}

+ 94
- 0
vendor/github.com/toorop/go-dkim/errors.go 查看文件

@@ -0,0 +1,94 @@
1
+package dkim
2
+
3
+import (
4
+	"errors"
5
+)
6
+
7
+var (
8
+	// ErrSignPrivateKeyRequired when there not private key in config
9
+	ErrSignPrivateKeyRequired = errors.New("PrivateKey is required")
10
+
11
+	// ErrSignDomainRequired when there is no domain defined in config
12
+	ErrSignDomainRequired = errors.New("Domain is required")
13
+
14
+	// ErrSignSelectorRequired when there is no Selcteir defined in config
15
+	ErrSignSelectorRequired = errors.New("Selector is required")
16
+
17
+	// ErrSignHeaderShouldContainsFrom If Headers is specified it should at least contain 'from'
18
+	ErrSignHeaderShouldContainsFrom = errors.New("header must contains 'from' field")
19
+
20
+	// ErrSignBadCanonicalization If bad Canonicalization parameter
21
+	ErrSignBadCanonicalization = errors.New("bad Canonicalization parameter")
22
+
23
+	// ErrCandNotParsePrivateKey when unable to parse private key
24
+	ErrCandNotParsePrivateKey = errors.New("can not parse private key, check format (pem) and validity")
25
+
26
+	// ErrSignBadAlgo Bad algorithm
27
+	ErrSignBadAlgo = errors.New("bad algorithm. Only rsa-sha1 or rsa-sha256 are permitted")
28
+
29
+	// ErrBadMailFormat unable to parse mail
30
+	ErrBadMailFormat = errors.New("bad mail format")
31
+
32
+	// ErrBadMailFormatHeaders bad headers format (not DKIM Header)
33
+	ErrBadMailFormatHeaders = errors.New("bad mail format found in headers")
34
+
35
+	// ErrBadDKimTagLBodyTooShort bad l tag
36
+	ErrBadDKimTagLBodyTooShort = errors.New("bad tag l or bodyLength option. Body length < l value")
37
+
38
+	// ErrDkimHeaderBadFormat when errors found in DKIM header
39
+	ErrDkimHeaderBadFormat = errors.New("bad DKIM header format")
40
+
41
+	// ErrDkimHeaderNotFound when there's no DKIM-Signature header in an email we have to verify
42
+	ErrDkimHeaderNotFound = errors.New("no DKIM-Signature header field found ")
43
+
44
+	// ErrDkimHeaderBTagNotFound when there's no b tag
45
+	ErrDkimHeaderBTagNotFound = errors.New("no tag 'b' found in dkim header")
46
+
47
+	// ErrDkimHeaderNoFromInHTag when from is missing in h tag
48
+	ErrDkimHeaderNoFromInHTag = errors.New("'from' header is missing in h tag")
49
+
50
+	// ErrDkimHeaderMissingRequiredTag when a required tag is missing
51
+	ErrDkimHeaderMissingRequiredTag = errors.New("signature missing required tag")
52
+
53
+	// ErrDkimHeaderDomainMismatch if i tag is not a sub domain of d tag
54
+	ErrDkimHeaderDomainMismatch = errors.New("domain mismatch")
55
+
56
+	// ErrDkimVersionNotsupported version not supported
57
+	ErrDkimVersionNotsupported = errors.New("incompatible version")
58
+
59
+	// Query method unsupported
60
+	errQueryMethodNotsupported = errors.New("query method not supported")
61
+
62
+	// ErrVerifyBodyHash when body hash doesn't verify
63
+	ErrVerifyBodyHash = errors.New("body hash did not verify")
64
+
65
+	// ErrVerifyNoKeyForSignature no key
66
+	ErrVerifyNoKeyForSignature = errors.New("no key for verify")
67
+
68
+	// ErrVerifyKeyUnavailable when service (dns) is anavailable
69
+	ErrVerifyKeyUnavailable = errors.New("key unavailable")
70
+
71
+	// ErrVerifyTagVMustBeTheFirst if present the v tag must be the firts in the record
72
+	ErrVerifyTagVMustBeTheFirst = errors.New("pub key syntax error: v tag must be the first")
73
+
74
+	// ErrVerifyVersionMusBeDkim1 if présent flag v (version) must be DKIM1
75
+	ErrVerifyVersionMusBeDkim1 = errors.New("flag v must be set to DKIM1")
76
+
77
+	// ErrVerifyBadKeyType bad type for pub key (only rsa is accepted)
78
+	ErrVerifyBadKeyType = errors.New("bad type for key type")
79
+
80
+	// ErrVerifyRevokedKey key(s) for this selector is revoked (p is empty)
81
+	ErrVerifyRevokedKey = errors.New("revoked key")
82
+
83
+	// ErrVerifyBadKey when we can't parse pubkey
84
+	ErrVerifyBadKey = errors.New("unable to parse pub key")
85
+
86
+	// ErrVerifyNoKey when no key is found on DNS record
87
+	ErrVerifyNoKey = errors.New("no public key found in DNS TXT")
88
+
89
+	// ErrVerifySignatureHasExpired when signature has expired
90
+	ErrVerifySignatureHasExpired = errors.New("signature has expired")
91
+
92
+	// ErrVerifyInappropriateHashAlgo when h tag in pub key doesn't contain hash algo from a tag of DKIM header
93
+	ErrVerifyInappropriateHashAlgo = errors.New("inappropriate has algorithm")
94
+)

+ 181
- 0
vendor/github.com/toorop/go-dkim/pubKeyRep.go 查看文件

@@ -0,0 +1,181 @@
1
+package dkim
2
+
3
+import (
4
+	"crypto/rsa"
5
+	"crypto/x509"
6
+	"encoding/base64"
7
+	"io/ioutil"
8
+	"mime/quotedprintable"
9
+	"net"
10
+	"strings"
11
+)
12
+
13
+// PubKeyRep represents a parsed version of public key record
14
+type PubKeyRep struct {
15
+	Version      string
16
+	HashAlgo     []string
17
+	KeyType      string
18
+	Note         string
19
+	PubKey       rsa.PublicKey
20
+	ServiceType  []string
21
+	FlagTesting  bool // flag y
22
+	FlagIMustBeD bool // flag i
23
+}
24
+
25
+// DNSOptions holds settings for looking up DNS records
26
+type DNSOptions struct {
27
+	netLookupTXT func(name string) ([]string, error)
28
+}
29
+
30
+// DNSOpt represents an optional setting for looking up DNS records
31
+type DNSOpt interface {
32
+	apply(*DNSOptions)
33
+}
34
+
35
+type dnsOpt func(*DNSOptions)
36
+
37
+func (opt dnsOpt) apply(dnsOpts *DNSOptions) {
38
+	opt(dnsOpts)
39
+}
40
+
41
+// DNSOptLookupTXT sets the function to use to lookup TXT records.
42
+//
43
+// This should probably only be used in tests.
44
+func DNSOptLookupTXT(netLookupTXT func(name string) ([]string, error)) DNSOpt {
45
+	return dnsOpt(func(opts *DNSOptions) {
46
+		opts.netLookupTXT = netLookupTXT
47
+	})
48
+}
49
+
50
+// NewPubKeyRespFromDNS retrieves the TXT record from DNS based on the specified domain and selector
51
+// and parses it.
52
+func NewPubKeyRespFromDNS(selector, domain string, opts ...DNSOpt) (*PubKeyRep, verifyOutput, error) {
53
+	dnsOpts := DNSOptions{}
54
+
55
+	for _, opt := range opts {
56
+		opt.apply(&dnsOpts)
57
+	}
58
+
59
+	if dnsOpts.netLookupTXT == nil {
60
+		dnsOpts.netLookupTXT = net.LookupTXT
61
+	}
62
+
63
+	txt, err := dnsOpts.netLookupTXT(selector + "._domainkey." + domain)
64
+	if err != nil {
65
+		if strings.HasSuffix(err.Error(), "no such host") {
66
+			return nil, PERMFAIL, ErrVerifyNoKeyForSignature
67
+		}
68
+
69
+		return nil, TEMPFAIL, ErrVerifyKeyUnavailable
70
+	}
71
+
72
+	// empty record
73
+	if len(txt) == 0 {
74
+		return nil, PERMFAIL, ErrVerifyNoKeyForSignature
75
+	}
76
+
77
+	// parsing, we keep the first record
78
+	// TODO: if there is multiple record
79
+
80
+	return NewPubKeyResp(txt[0])
81
+}
82
+
83
+// NewPubKeyResp parses DKIM record (usually from DNS)
84
+func NewPubKeyResp(dkimRecord string) (*PubKeyRep, verifyOutput, error) {
85
+	pkr := new(PubKeyRep)
86
+	pkr.Version = "DKIM1"
87
+	pkr.HashAlgo = []string{"sha1", "sha256"}
88
+	pkr.KeyType = "rsa"
89
+	pkr.FlagTesting = false
90
+	pkr.FlagIMustBeD = false
91
+
92
+	p := strings.Split(dkimRecord, ";")
93
+	for i, data := range p {
94
+		keyVal := strings.SplitN(data, "=", 2)
95
+		val := ""
96
+		if len(keyVal) > 1 {
97
+			val = strings.TrimSpace(keyVal[1])
98
+		}
99
+		switch strings.ToLower(strings.TrimSpace(keyVal[0])) {
100
+		case "v":
101
+			// RFC: is this tag is specified it MUST be the first in the record
102
+			if i != 0 {
103
+				return nil, PERMFAIL, ErrVerifyTagVMustBeTheFirst
104
+			}
105
+			pkr.Version = val
106
+			if pkr.Version != "DKIM1" {
107
+				return nil, PERMFAIL, ErrVerifyVersionMusBeDkim1
108
+			}
109
+		case "h":
110
+			p := strings.Split(strings.ToLower(val), ":")
111
+			pkr.HashAlgo = []string{}
112
+			for _, h := range p {
113
+				h = strings.TrimSpace(h)
114
+				if h == "sha1" || h == "sha256" {
115
+					pkr.HashAlgo = append(pkr.HashAlgo, h)
116
+				}
117
+			}
118
+			// if empty switch back to default
119
+			if len(pkr.HashAlgo) == 0 {
120
+				pkr.HashAlgo = []string{"sha1", "sha256"}
121
+			}
122
+		case "k":
123
+			if strings.ToLower(val) != "rsa" {
124
+				return nil, PERMFAIL, ErrVerifyBadKeyType
125
+			}
126
+		case "n":
127
+			qp, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(val)))
128
+			if err == nil {
129
+				val = string(qp)
130
+			}
131
+			pkr.Note = val
132
+		case "p":
133
+			rawkey := val
134
+			if rawkey == "" {
135
+				return nil, PERMFAIL, ErrVerifyRevokedKey
136
+			}
137
+			un64, err := base64.StdEncoding.DecodeString(rawkey)
138
+			if err != nil {
139
+				return nil, PERMFAIL, ErrVerifyBadKey
140
+			}
141
+			pk, err := x509.ParsePKIXPublicKey(un64)
142
+			if pk, ok := pk.(*rsa.PublicKey); ok {
143
+				pkr.PubKey = *pk
144
+			}
145
+		case "s":
146
+			t := strings.Split(strings.ToLower(val), ":")
147
+			for _, tt := range t {
148
+				tt = strings.TrimSpace(tt)
149
+				switch tt {
150
+				case "*":
151
+					pkr.ServiceType = append(pkr.ServiceType, "all")
152
+				case "email":
153
+					pkr.ServiceType = append(pkr.ServiceType, tt)
154
+				}
155
+			}
156
+		case "t":
157
+			flags := strings.Split(strings.ToLower(val), ":")
158
+			for _, flag := range flags {
159
+				flag = strings.TrimSpace(flag)
160
+				switch flag {
161
+				case "y":
162
+					pkr.FlagTesting = true
163
+				case "s":
164
+					pkr.FlagIMustBeD = true
165
+				}
166
+			}
167
+		}
168
+	}
169
+
170
+	// if no pubkey
171
+	if pkr.PubKey == (rsa.PublicKey{}) {
172
+		return nil, PERMFAIL, ErrVerifyNoKey
173
+	}
174
+
175
+	// No service type
176
+	if len(pkr.ServiceType) == 0 {
177
+		pkr.ServiceType = []string{"all"}
178
+	}
179
+
180
+	return pkr, SUCCESS, nil
181
+}

+ 4
- 0
vendor/github.com/toorop/go-dkim/watch 查看文件

@@ -0,0 +1,4 @@
1
+while true
2
+do 
3
+inotifywait -q -r -e modify,attrib,close_write,move,create,delete . && echo "--------------" && go test -v
4
+done

+ 3
- 0
vendor/modules.txt 查看文件

@@ -59,6 +59,9 @@ github.com/tidwall/rtree
59 59
 github.com/tidwall/rtree/base
60 60
 # github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563
61 61
 github.com/tidwall/tinyqueue
62
+# github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb
63
+## explicit
64
+github.com/toorop/go-dkim
62 65
 # golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
63 66
 ## explicit
64 67
 golang.org/x/crypto/bcrypt

正在加载...
取消
保存