mirror of
				https://github.com/ergochat/ergo.git
				synced 2025-10-31 05:47:22 +01:00 
			
		
		
		
	
							parent
							
								
									6e630a0b5c
								
							
						
					
					
						commit
						ee05a4324d
					
				
							
								
								
									
										1
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Makefile
									
									
									
									
									
								
							| @ -22,6 +22,7 @@ test: | ||||
| 	cd irc/caps && go test . && go vet . | ||||
| 	cd irc/cloaks && go test . && go vet . | ||||
| 	cd irc/connection_limits && go test . && go vet . | ||||
| 	cd irc/email && go test . && go vet . | ||||
| 	cd irc/history && go test . && go vet . | ||||
| 	cd irc/isupport && go test . && go vet . | ||||
| 	cd irc/modes && go test . && go vet . | ||||
|  | ||||
| @ -280,16 +280,24 @@ accounts: | ||||
|         enabled-callbacks: | ||||
|             - none # no verification needed, will instantly register successfully | ||||
| 
 | ||||
|         # example configuration for sending verification emails via a local mail relay | ||||
|         # example configuration for sending verification emails | ||||
|         # callbacks: | ||||
|         #     mailto: | ||||
|         #         server: localhost | ||||
|         #         port: 25 | ||||
|         #         tls: | ||||
|         #             enabled: false | ||||
|         #         username: "" | ||||
|         #         password: "" | ||||
|         #         sender: "admin@my.network" | ||||
|         #         require-tls: true | ||||
|         #         helo-domain: "my.network" # defaults to server name if unset | ||||
|         #         dkim: | ||||
|         #             domain: "my.network" | ||||
|         #             selector: "20200229" | ||||
|         #             key-file: "dkim.pem" | ||||
|         #         # to use an MTA/smarthost instead of sending email directly: | ||||
|         #         # mta: | ||||
|         #         #     server: localhost | ||||
|         #         #     port: 25 | ||||
|         #         #     username: "admin" | ||||
|         #         #     password: "hunter2" | ||||
|         #         blacklist-regexes: | ||||
|         #         #    - ".*@mailinator.com" | ||||
| 
 | ||||
|     # throttle account login attempts (to prevent either password guessing, or DoS | ||||
|     # attacks on the server aimed at forcing repeated expensive bcrypt computations) | ||||
|  | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -18,6 +18,7 @@ require ( | ||||
| 	github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a | ||||
| 	github.com/stretchr/testify v1.4.0 // indirect | ||||
| 	github.com/tidwall/buntdb v1.1.2 | ||||
| 	github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb | ||||
| 	golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 | ||||
| 	golang.org/x/text v0.3.2 | ||||
| 	gopkg.in/yaml.v2 v2.2.8 | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @ -65,6 +65,8 @@ github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2K | ||||
| github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= | ||||
| github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= | ||||
| github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= | ||||
| github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb h1:ilDZC+k9r67aJqSOalZLtEVLO7Cmmsq5ftfcvLirc24= | ||||
| github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4= | ||||
| golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
|  | ||||
| @ -4,9 +4,9 @@ | ||||
| package irc | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/smtp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| @ -16,6 +16,7 @@ import ( | ||||
| 
 | ||||
| 	"github.com/oragono/oragono/irc/caps" | ||||
| 	"github.com/oragono/oragono/irc/connection_limits" | ||||
| 	"github.com/oragono/oragono/irc/email" | ||||
| 	"github.com/oragono/oragono/irc/ldap" | ||||
| 	"github.com/oragono/oragono/irc/passwd" | ||||
| 	"github.com/oragono/oragono/irc/utils" | ||||
| @ -483,7 +484,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue) | ||||
| 	code, err := am.dispatchCallback(client, account, callbackNamespace, callbackValue) | ||||
| 	if err != nil { | ||||
| 		am.Unregister(casefoldedAccount, true) | ||||
| 		return errCallbackFailed | ||||
| @ -698,17 +699,17 @@ func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasP | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) { | ||||
| func (am *AccountManager) dispatchCallback(client *Client, account string, callbackNamespace string, callbackValue string) (string, error) { | ||||
| 	if callbackNamespace == "*" || callbackNamespace == "none" || callbackNamespace == "admin" { | ||||
| 		return "", nil | ||||
| 	} else if callbackNamespace == "mailto" { | ||||
| 		return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue) | ||||
| 		return am.dispatchMailtoCallback(client, account, callbackValue) | ||||
| 	} else { | ||||
| 		return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) { | ||||
| func (am *AccountManager) dispatchMailtoCallback(client *Client, account string, callbackValue string) (code string, err error) { | ||||
| 	config := am.server.Config().Accounts.Registration.Callbacks.Mailto | ||||
| 	code = utils.GenerateSecretToken() | ||||
| 
 | ||||
| @ -716,34 +717,27 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou | ||||
| 	if subject == "" { | ||||
| 		subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name) | ||||
| 	} | ||||
| 	messageStrings := []string{ | ||||
| 		fmt.Sprintf("From: %s\r\n", config.Sender), | ||||
| 		fmt.Sprintf("To: %s\r\n", callbackValue), | ||||
| 		fmt.Sprintf("Subject: %s\r\n", subject), | ||||
| 		"\r\n", // end headers, begin message body | ||||
| 		fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n", | ||||
| 		fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n", | ||||
| 		"\r\n", | ||||
| 		client.t("To verify your account, issue the following command:") + "\r\n", | ||||
| 		fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n", | ||||
| 	} | ||||
| 
 | ||||
| 	var message []byte | ||||
| 	for i := 0; i < len(messageStrings); i++ { | ||||
| 		message = append(message, []byte(messageStrings[i])...) | ||||
| 	} | ||||
| 	addr := fmt.Sprintf("%s:%d", config.Server, config.Port) | ||||
| 	var auth smtp.Auth | ||||
| 	if config.Username != "" && config.Password != "" { | ||||
| 		auth = smtp.PlainAuth("", config.Username, config.Password, config.Server) | ||||
| 	var message bytes.Buffer | ||||
| 	fmt.Fprintf(&message, "From: %s\r\n", config.Sender) | ||||
| 	fmt.Fprintf(&message, "To: %s\r\n", callbackValue) | ||||
| 	if config.DKIM.Domain != "" { | ||||
| 		fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), config.DKIM.Domain) | ||||
| 	} | ||||
| 	fmt.Fprintf(&message, "Subject: %s\r\n", subject) | ||||
| 	message.WriteString("\r\n") // blank line: end headers, begin message body | ||||
| 	fmt.Fprintf(&message, client.t("Account: %s"), account) | ||||
| 	message.WriteString("\r\n") | ||||
| 	fmt.Fprintf(&message, client.t("Verification code: %s"), code) | ||||
| 	message.WriteString("\r\n") | ||||
| 	message.WriteString("\r\n") | ||||
| 	message.WriteString(client.t("To verify your account, issue the following command:")) | ||||
| 	message.WriteString("\r\n") | ||||
| 	fmt.Fprintf(&message, "/MSG NickServ VERIFY %s %s\r\n", account, code) | ||||
| 
 | ||||
| 	// TODO: this will never send the password in plaintext over a nonlocal link, | ||||
| 	// but it might send the email in plaintext, regardless of the value of | ||||
| 	// config.TLS.InsecureSkipVerify | ||||
| 	err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message) | ||||
| 	err = email.SendMail(config, callbackValue, message.Bytes()) | ||||
| 	if err != nil { | ||||
| 		am.server.logger.Error("internal", "Failed to dispatch e-mail", err.Error()) | ||||
| 		am.server.logger.Error("internal", "Failed to dispatch e-mail to", callbackValue, err.Error()) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| @ -24,6 +24,7 @@ import ( | ||||
| 	"github.com/oragono/oragono/irc/cloaks" | ||||
| 	"github.com/oragono/oragono/irc/connection_limits" | ||||
| 	"github.com/oragono/oragono/irc/custime" | ||||
| 	"github.com/oragono/oragono/irc/email" | ||||
| 	"github.com/oragono/oragono/irc/isupport" | ||||
| 	"github.com/oragono/oragono/irc/languages" | ||||
| 	"github.com/oragono/oragono/irc/ldap" | ||||
| @ -290,20 +291,7 @@ type AccountRegistrationConfig struct { | ||||
| 	EnabledCredentialTypes []string         `yaml:"-"` | ||||
| 	VerifyTimeout          custime.Duration `yaml:"verify-timeout"` | ||||
| 	Callbacks              struct { | ||||
| 		Mailto struct { | ||||
| 			Server string | ||||
| 			Port   int | ||||
| 			TLS    struct { | ||||
| 				Enabled            bool | ||||
| 				InsecureSkipVerify bool   `yaml:"insecure_skip_verify"` | ||||
| 				ServerName         string `yaml:"servername"` | ||||
| 			} | ||||
| 			Username             string | ||||
| 			Password             string | ||||
| 			Sender               string | ||||
| 			VerifyMessageSubject string `yaml:"verify-message-subject"` | ||||
| 			VerifyMessage        string `yaml:"verify-message"` | ||||
| 		} | ||||
| 		Mailto email.MailtoConfig | ||||
| 	} | ||||
| 	BcryptCost uint `yaml:"bcrypt-cost"` | ||||
| } | ||||
| @ -975,14 +963,24 @@ func LoadConfig(filename string) (config *Config, err error) { | ||||
| 
 | ||||
| 	// hardcode this for now | ||||
| 	config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"} | ||||
| 	mailtoEnabled := false | ||||
| 	for i, name := range config.Accounts.Registration.EnabledCallbacks { | ||||
| 		if name == "none" { | ||||
| 			// we store "none" as "*" internally | ||||
| 			config.Accounts.Registration.EnabledCallbacks[i] = "*" | ||||
| 		} else if name == "mailto" { | ||||
| 			mailtoEnabled = true | ||||
| 		} | ||||
| 	} | ||||
| 	sort.Strings(config.Accounts.Registration.EnabledCallbacks) | ||||
| 
 | ||||
| 	if mailtoEnabled { | ||||
| 		err := config.Accounts.Registration.Callbacks.Mailto.Postprocess(config.Server.Name) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	config.Accounts.RequireSasl.exemptedNets, err = utils.ParseNetList(config.Accounts.RequireSasl.Exempted) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("Could not parse require-sasl exempted nets: %v", err.Error()) | ||||
|  | ||||
							
								
								
									
										54
									
								
								irc/email/dkim.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								irc/email/dkim.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| // Copyright (c) 2020 Shivaram Lingamneni | ||||
| // released under the MIT license | ||||
| 
 | ||||
| package email | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	dkim "github.com/toorop/go-dkim" | ||||
| 	"io/ioutil" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrMissingFields = errors.New("DKIM config is missing fields") | ||||
| ) | ||||
| 
 | ||||
| type DKIMConfig struct { | ||||
| 	Domain   string | ||||
| 	Selector string | ||||
| 	KeyFile  string `yaml:"key-file"` | ||||
| 	keyBytes []byte | ||||
| } | ||||
| 
 | ||||
| func (dkim *DKIMConfig) Postprocess() (err error) { | ||||
| 	if dkim.Domain != "" { | ||||
| 		if dkim.Selector == "" || dkim.KeyFile == "" { | ||||
| 			return ErrMissingFields | ||||
| 		} | ||||
| 		dkim.keyBytes, err = ioutil.ReadFile(dkim.KeyFile) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var defaultOptions = dkim.SigOptions{ | ||||
| 	Version:               1, | ||||
| 	Canonicalization:      "relaxed/relaxed", | ||||
| 	Algo:                  "rsa-sha256", | ||||
| 	Headers:               []string{"from", "to", "subject", "message-id"}, | ||||
| 	BodyLength:            0, | ||||
| 	QueryMethods:          []string{"dns/txt"}, | ||||
| 	AddSignatureTimestamp: true, | ||||
| 	SignatureExpireIn:     0, | ||||
| } | ||||
| 
 | ||||
| func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) { | ||||
| 	options := defaultOptions | ||||
| 	options.PrivateKey = dkimConfig.keyBytes | ||||
| 	options.Domain = dkimConfig.Domain | ||||
| 	options.Selector = dkimConfig.Selector | ||||
| 	err = dkim.Sign(&message, options) | ||||
| 	return message, err | ||||
| } | ||||
							
								
								
									
										124
									
								
								irc/email/email.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								irc/email/email.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | ||||
| // Copyright (c) 2020 Shivaram Lingamneni | ||||
| // released under the MIT license | ||||
| 
 | ||||
| package email | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/oragono/oragono/irc/smtp" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrBlacklistedAddress = errors.New("Email address is blacklisted") | ||||
| 	ErrInvalidAddress     = errors.New("Email address is blacklisted") | ||||
| 	ErrNoMXRecord         = errors.New("Couldn't resolve MX record") | ||||
| ) | ||||
| 
 | ||||
| type MTAConfig struct { | ||||
| 	Server   string | ||||
| 	Port     int | ||||
| 	Username string | ||||
| 	Password string | ||||
| } | ||||
| 
 | ||||
| type MailtoConfig struct { | ||||
| 	// legacy config format assumed the use of an MTA/smarthost, | ||||
| 	// so server, port, etc. appear directly at top level | ||||
| 	// XXX: see https://github.com/go-yaml/yaml/issues/63 | ||||
| 	MTAConfig            `yaml:",inline"` | ||||
| 	Sender               string | ||||
| 	HeloDomain           string `yaml:"helo-domain"` | ||||
| 	RequireTLS           bool   `yaml:"require-tls"` | ||||
| 	VerifyMessageSubject string `yaml:"verify-message-subject"` | ||||
| 	DKIM                 DKIMConfig | ||||
| 	MTAReal              MTAConfig `yaml:"mta"` | ||||
| 	BlacklistRegexes     []string  `yaml:"blacklist-regexes"` | ||||
| 	blacklistRegexes     []*regexp.Regexp | ||||
| } | ||||
| 
 | ||||
| func (config *MailtoConfig) Postprocess(heloDomain string) (err error) { | ||||
| 	if config.Sender == "" { | ||||
| 		return errors.New("Invalid mailto sender address") | ||||
| 	} | ||||
| 
 | ||||
| 	// check for MTA config fields at top level, | ||||
| 	// copy to MTAReal if present | ||||
| 	if config.Server != "" && config.MTAReal.Server == "" { | ||||
| 		config.MTAReal = config.MTAConfig | ||||
| 	} | ||||
| 
 | ||||
| 	if config.HeloDomain == "" { | ||||
| 		config.HeloDomain = heloDomain | ||||
| 	} | ||||
| 
 | ||||
| 	for _, reg := range config.BlacklistRegexes { | ||||
| 		compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		config.blacklistRegexes = append(config.blacklistRegexes, compiled) | ||||
| 	} | ||||
| 
 | ||||
| 	if config.MTAConfig.Server != "" { | ||||
| 		// smarthost, nothing more to validate | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	return config.DKIM.Postprocess() | ||||
| } | ||||
| 
 | ||||
| // get the preferred MX record hostname, "" on error | ||||
| func lookupMX(domain string) (server string) { | ||||
| 	var minPref uint16 | ||||
| 	results, err := net.LookupMX(domain) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	for _, result := range results { | ||||
| 		if minPref == 0 || result.Pref < minPref { | ||||
| 			server, minPref = result.Host, result.Pref | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) { | ||||
| 	for _, reg := range config.blacklistRegexes { | ||||
| 		if reg.MatchString(recipient) { | ||||
| 			return ErrBlacklistedAddress | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if config.DKIM.Domain != "" { | ||||
| 		msg, err = DKIMSign(msg, config.DKIM) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var addr string | ||||
| 	var auth smtp.Auth | ||||
| 	if config.MTAReal.Server != "" { | ||||
| 		addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port) | ||||
| 		if config.MTAReal.Username != "" && config.MTAReal.Password != "" { | ||||
| 			auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server) | ||||
| 		} | ||||
| 	} else { | ||||
| 		idx := strings.IndexByte(recipient, '@') | ||||
| 		if idx == -1 { | ||||
| 			return ErrInvalidAddress | ||||
| 		} | ||||
| 		mx := lookupMX(recipient[idx+1:]) | ||||
| 		if mx == "" { | ||||
| 			return ErrNoMXRecord | ||||
| 		} | ||||
| 		addr = fmt.Sprintf("%s:smtp", mx) | ||||
| 	} | ||||
| 
 | ||||
| 	return smtp.SendMail(addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg, config.RequireTLS) | ||||
| } | ||||
| @ -316,7 +316,8 @@ var testHookStartTLS func(*tls.Config) // nil, except for tests | ||||
| // attachments (see the mime/multipart package), or other mail | ||||
| // functionality. Higher-level packages exist outside of the standard | ||||
| // library. | ||||
| func SendMail(addr string, a Auth, from string, to []string, msg []byte) error { | ||||
| // XXX: modified in Oragono to add `requireTLS` and `heloDomain` arguments | ||||
| func SendMail(addr string, a Auth, heloDomain string, from string, to []string, msg []byte, requireTLS bool) error { | ||||
| 	if err := validateLine(from); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @ -330,7 +331,7 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer c.Close() | ||||
| 	if err = c.hello(); err != nil { | ||||
| 	if err = c.Hello(heloDomain); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if ok, _ := c.Extension("STARTTLS"); ok { | ||||
| @ -341,6 +342,8 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error { | ||||
| 		if err = c.StartTLS(config); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else if requireTLS { | ||||
| 		return errors.New("TLS required, but not negotiated") | ||||
| 	} | ||||
| 	if a != nil && c.ext != nil { | ||||
| 		if _, ok := c.ext["AUTH"]; !ok { | ||||
|  | ||||
							
								
								
									
										22
									
								
								oragono.yaml
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								oragono.yaml
									
									
									
									
									
								
							| @ -301,16 +301,24 @@ accounts: | ||||
|         enabled-callbacks: | ||||
|             - none # no verification needed, will instantly register successfully | ||||
| 
 | ||||
|         # example configuration for sending verification emails via a local mail relay | ||||
|         # example configuration for sending verification emails | ||||
|         # callbacks: | ||||
|         #     mailto: | ||||
|         #         server: localhost | ||||
|         #         port: 25 | ||||
|         #         tls: | ||||
|         #             enabled: false | ||||
|         #         username: "" | ||||
|         #         password: "" | ||||
|         #         sender: "admin@my.network" | ||||
|         #         require-tls: true | ||||
|         #         helo-domain: "my.network" # defaults to server name if unset | ||||
|         #         dkim: | ||||
|         #             domain: "my.network" | ||||
|         #             selector: "20200229" | ||||
|         #             key-file: "dkim.pem" | ||||
|         #         # to use an MTA/smarthost instead of sending email directly: | ||||
|         #         # mta: | ||||
|         #         #     server: localhost | ||||
|         #         #     port: 25 | ||||
|         #         #     username: "admin" | ||||
|         #         #     password: "hunter2" | ||||
|         #         blacklist-regexes: | ||||
|         #         #    - ".*@mailinator.com" | ||||
| 
 | ||||
|     # throttle account login attempts (to prevent either password guessing, or DoS | ||||
|     # attacks on the server aimed at forcing repeated expensive bcrypt computations) | ||||
|  | ||||
							
								
								
									
										24
									
								
								vendor/github.com/toorop/go-dkim/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								vendor/github.com/toorop/go-dkim/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| # Compiled Object files, Static and Dynamic libs (Shared Objects) | ||||
| *.o | ||||
| *.a | ||||
| *.so | ||||
| 
 | ||||
| # Folders | ||||
| _obj | ||||
| _test | ||||
| 
 | ||||
| # Architecture specific extensions/prefixes | ||||
| *.[568vq] | ||||
| [568vq].out | ||||
| 
 | ||||
| *.cgo1.go | ||||
| *.cgo2.c | ||||
| _cgo_defun.c | ||||
| _cgo_gotypes.go | ||||
| _cgo_export.* | ||||
| 
 | ||||
| _testmain.go | ||||
| 
 | ||||
| *.exe | ||||
| *.test | ||||
| *.prof | ||||
							
								
								
									
										22
									
								
								vendor/github.com/toorop/go-dkim/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								vendor/github.com/toorop/go-dkim/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| The MIT License (MIT) | ||||
| 
 | ||||
| Copyright (c) 2015 Stéphane Depierrepont | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
| 
 | ||||
							
								
								
									
										56
									
								
								vendor/github.com/toorop/go-dkim/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								vendor/github.com/toorop/go-dkim/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| # 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) | ||||
							
								
								
									
										557
									
								
								vendor/github.com/toorop/go-dkim/dkim.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										557
									
								
								vendor/github.com/toorop/go-dkim/dkim.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,557 @@ | ||||
| // Package dkim provides tools for signing and verify a email according to RFC 6376 | ||||
| package dkim | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"container/list" | ||||
| 	"crypto" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/pem" | ||||
| 	"hash" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	CRLF                = "\r\n" | ||||
| 	TAB                 = " " | ||||
| 	FWS                 = CRLF + TAB | ||||
| 	MaxHeaderLineLength = 70 | ||||
| ) | ||||
| 
 | ||||
| type verifyOutput int | ||||
| 
 | ||||
| const ( | ||||
| 	SUCCESS verifyOutput = 1 + iota | ||||
| 	PERMFAIL | ||||
| 	TEMPFAIL | ||||
| 	NOTSIGNED | ||||
| 	TESTINGSUCCESS | ||||
| 	TESTINGPERMFAIL | ||||
| 	TESTINGTEMPFAIL | ||||
| ) | ||||
| 
 | ||||
| // sigOptions represents signing options | ||||
| type SigOptions struct { | ||||
| 
 | ||||
| 	// DKIM version (default 1) | ||||
| 	Version uint | ||||
| 
 | ||||
| 	// Private key used for signing (required) | ||||
| 	PrivateKey []byte | ||||
| 
 | ||||
| 	// Domain (required) | ||||
| 	Domain string | ||||
| 
 | ||||
| 	// Selector (required) | ||||
| 	Selector string | ||||
| 
 | ||||
| 	// The Agent of User IDentifier | ||||
| 	Auid string | ||||
| 
 | ||||
| 	// Message canonicalization (plain-text; OPTIONAL, default is | ||||
| 	// "simple/simple").  This tag informs the Verifier of the type of | ||||
| 	// canonicalization used to prepare the message for signing. | ||||
| 	Canonicalization string | ||||
| 
 | ||||
| 	// The algorithm used to generate the signature | ||||
| 	//"rsa-sha1" or "rsa-sha256" | ||||
| 	Algo string | ||||
| 
 | ||||
| 	// Signed header fields | ||||
| 	Headers []string | ||||
| 
 | ||||
| 	// Body length count( if set to 0 this tag is ommited in Dkim header) | ||||
| 	BodyLength uint | ||||
| 
 | ||||
| 	// Query Methods used to retrieve the public key | ||||
| 	QueryMethods []string | ||||
| 
 | ||||
| 	// Add a signature timestamp | ||||
| 	AddSignatureTimestamp bool | ||||
| 
 | ||||
| 	// Time validity of the signature (0=never) | ||||
| 	SignatureExpireIn uint64 | ||||
| 
 | ||||
| 	// CopiedHeaderFileds | ||||
| 	CopiedHeaderFields []string | ||||
| } | ||||
| 
 | ||||
| // NewSigOptions returns new sigoption with some defaults value | ||||
| func NewSigOptions() SigOptions { | ||||
| 	return SigOptions{ | ||||
| 		Version:               1, | ||||
| 		Canonicalization:      "simple/simple", | ||||
| 		Algo:                  "rsa-sha256", | ||||
| 		Headers:               []string{"from"}, | ||||
| 		BodyLength:            0, | ||||
| 		QueryMethods:          []string{"dns/txt"}, | ||||
| 		AddSignatureTimestamp: true, | ||||
| 		SignatureExpireIn:     0, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Sign signs an email | ||||
| func Sign(email *[]byte, options SigOptions) error { | ||||
| 	var privateKey *rsa.PrivateKey | ||||
| 
 | ||||
| 	// PrivateKey | ||||
| 	if len(options.PrivateKey) == 0 { | ||||
| 		return ErrSignPrivateKeyRequired | ||||
| 	} | ||||
| 	d, _ := pem.Decode(options.PrivateKey) | ||||
| 	if d == nil { | ||||
| 		return ErrCandNotParsePrivateKey | ||||
| 	} | ||||
| 	key, err := x509.ParsePKCS1PrivateKey(d.Bytes) | ||||
| 	if err != nil { | ||||
| 		return ErrCandNotParsePrivateKey | ||||
| 	} | ||||
| 	privateKey = key | ||||
| 
 | ||||
| 	// Domain required | ||||
| 	if options.Domain == "" { | ||||
| 		return ErrSignDomainRequired | ||||
| 	} | ||||
| 
 | ||||
| 	// Selector required | ||||
| 	if options.Selector == "" { | ||||
| 		return ErrSignSelectorRequired | ||||
| 	} | ||||
| 
 | ||||
| 	// Canonicalization | ||||
| 	options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Algo | ||||
| 	options.Algo = strings.ToLower(options.Algo) | ||||
| 	if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" { | ||||
| 		return ErrSignBadAlgo | ||||
| 	} | ||||
| 
 | ||||
| 	// Header must contain "from" | ||||
| 	hasFrom := false | ||||
| 	for i, h := range options.Headers { | ||||
| 		h = strings.ToLower(h) | ||||
| 		options.Headers[i] = h | ||||
| 		if h == "from" { | ||||
| 			hasFrom = true | ||||
| 		} | ||||
| 	} | ||||
| 	if !hasFrom { | ||||
| 		return ErrSignHeaderShouldContainsFrom | ||||
| 	} | ||||
| 
 | ||||
| 	// Normalize | ||||
| 	headers, body, err := canonicalize(email, options.Canonicalization, options.Headers) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	signHash := strings.Split(options.Algo, "-") | ||||
| 
 | ||||
| 	// hash body | ||||
| 	bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Get dkim header base | ||||
| 	dkimHeader := newDkimHeaderBySigOptions(options) | ||||
| 	dHeader := dkimHeader.getHeaderBaseForSigning(bodyHash) | ||||
| 
 | ||||
| 	canonicalizations := strings.Split(options.Canonicalization, "/") | ||||
| 	dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	headers = append(headers, []byte(dHeaderCanonicalized)...) | ||||
| 	headers = bytes.TrimRight(headers, " \r\n") | ||||
| 
 | ||||
| 	// sign | ||||
| 	sig, err := getSignature(&headers, privateKey, signHash[1]) | ||||
| 
 | ||||
| 	// add to DKIM-Header | ||||
| 	subh := "" | ||||
| 	l := len(subh) | ||||
| 	for _, c := range sig { | ||||
| 		subh += string(c) | ||||
| 		l++ | ||||
| 		if l >= MaxHeaderLineLength { | ||||
| 			dHeader += subh + FWS | ||||
| 			subh = "" | ||||
| 			l = 0 | ||||
| 		} | ||||
| 	} | ||||
| 	dHeader += subh + CRLF | ||||
| 	*email = append([]byte(dHeader), *email...) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Verify verifies an email an return | ||||
| // state: SUCCESS or PERMFAIL or TEMPFAIL, TESTINGSUCCESS, TESTINGPERMFAIL | ||||
| // TESTINGTEMPFAIL or NOTSIGNED | ||||
| // error: if an error occurs during verification | ||||
| func Verify(email *[]byte, opts ...DNSOpt) (verifyOutput, error) { | ||||
| 	// parse email | ||||
| 	dkimHeader, err := newDkimHeaderFromEmail(email) | ||||
| 	if err != nil { | ||||
| 		if err == ErrDkimHeaderNotFound { | ||||
| 			return NOTSIGNED, ErrDkimHeaderNotFound | ||||
| 		} | ||||
| 		return PERMFAIL, err | ||||
| 	} | ||||
| 
 | ||||
| 	// we do not set query method because if it's others, validation failed earlier | ||||
| 	pubKey, verifyOutputOnError, err := NewPubKeyRespFromDNS(dkimHeader.Selector, dkimHeader.Domain, opts...) | ||||
| 	if err != nil { | ||||
| 		// fix https://github.com/toorop/go-dkim/issues/1 | ||||
| 		//return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting) | ||||
| 		return verifyOutputOnError, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Normalize | ||||
| 	headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) | ||||
| 	if err != nil { | ||||
| 		return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) | ||||
| 	} | ||||
| 	sigHash := strings.Split(dkimHeader.Algorithm, "-") | ||||
| 	// check if hash algo are compatible | ||||
| 	compatible := false | ||||
| 	for _, algo := range pubKey.HashAlgo { | ||||
| 		if sigHash[1] == algo { | ||||
| 			compatible = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if !compatible { | ||||
| 		return getVerifyOutput(PERMFAIL, ErrVerifyInappropriateHashAlgo, pubKey.FlagTesting) | ||||
| 	} | ||||
| 
 | ||||
| 	// expired ? | ||||
| 	if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() { | ||||
| 		return getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting) | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	//println("|" + string(body) + "|") | ||||
| 	// get body hash | ||||
| 	bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength) | ||||
| 	if err != nil { | ||||
| 		return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) | ||||
| 	} | ||||
| 	//println(bodyHash) | ||||
| 	if bodyHash != dkimHeader.BodyHash { | ||||
| 		return getVerifyOutput(PERMFAIL, ErrVerifyBodyHash, pubKey.FlagTesting) | ||||
| 	} | ||||
| 
 | ||||
| 	// compute sig | ||||
| 	dkimHeaderCano, err := canonicalizeHeader(dkimHeader.RawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0]) | ||||
| 	if err != nil { | ||||
| 		return getVerifyOutput(TEMPFAIL, err, pubKey.FlagTesting) | ||||
| 	} | ||||
| 	toSignStr := string(headers) + dkimHeaderCano | ||||
| 	toSign := bytes.TrimRight([]byte(toSignStr), " \r\n") | ||||
| 
 | ||||
| 	err = verifySignature(toSign, dkimHeader.SignatureData, &pubKey.PubKey, sigHash[1]) | ||||
| 	if err != nil { | ||||
| 		return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) | ||||
| 	} | ||||
| 	return SUCCESS, nil | ||||
| } | ||||
| 
 | ||||
| // getVerifyOutput returns output of verify fct according to the testing flag | ||||
| func getVerifyOutput(status verifyOutput, err error, flagTesting bool) (verifyOutput, error) { | ||||
| 	if !flagTesting { | ||||
| 		return status, err | ||||
| 	} | ||||
| 	switch status { | ||||
| 	case SUCCESS: | ||||
| 		return TESTINGSUCCESS, err | ||||
| 	case PERMFAIL: | ||||
| 		return TESTINGPERMFAIL, err | ||||
| 	case TEMPFAIL: | ||||
| 		return TESTINGTEMPFAIL, err | ||||
| 	} | ||||
| 	// should never happen but compilator sream whithout return | ||||
| 	return status, err | ||||
| } | ||||
| 
 | ||||
| // canonicalize returns canonicalized version of header and body | ||||
| func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, err error) { | ||||
| 	body = []byte{} | ||||
| 	rxReduceWS := regexp.MustCompile(`[ \t]+`) | ||||
| 
 | ||||
| 	rawHeaders, rawBody, err := getHeadersBody(email) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	canonicalizations := strings.Split(cano, "/") | ||||
| 
 | ||||
| 	// canonicalyze header | ||||
| 	headersList, err := getHeadersList(&rawHeaders) | ||||
| 
 | ||||
| 	// pour chaque header a conserver on traverse tous les headers dispo | ||||
| 	// If multi instance of a field we must keep it from the bottom to the top | ||||
| 	var match *list.Element | ||||
| 	headersToKeepList := list.New() | ||||
| 
 | ||||
| 	for _, headerToKeep := range h { | ||||
| 		match = nil | ||||
| 		headerToKeepToLower := strings.ToLower(headerToKeep) | ||||
| 		for e := headersList.Front(); e != nil; e = e.Next() { | ||||
| 			//fmt.Printf("|%s|\n", e.Value.(string)) | ||||
| 			t := strings.Split(e.Value.(string), ":") | ||||
| 			if strings.ToLower(t[0]) == headerToKeepToLower { | ||||
| 				match = e | ||||
| 			} | ||||
| 		} | ||||
| 		if match != nil { | ||||
| 			headersToKeepList.PushBack(match.Value.(string) + "\r\n") | ||||
| 			headersList.Remove(match) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	//if canonicalizations[0] == "simple" { | ||||
| 	for e := headersToKeepList.Front(); e != nil; e = e.Next() { | ||||
| 		cHeader, err := canonicalizeHeader(e.Value.(string), canonicalizations[0]) | ||||
| 		if err != nil { | ||||
| 			return headers, body, err | ||||
| 		} | ||||
| 		headers = append(headers, []byte(cHeader)...) | ||||
| 	} | ||||
| 	// canonicalyze body | ||||
| 	if canonicalizations[1] == "simple" { | ||||
| 		// simple | ||||
| 		// The "simple" body canonicalization algorithm ignores all empty lines | ||||
| 		// at the end of the message body.  An empty line is a line of zero | ||||
| 		// length after removal of the line terminator.  If there is no body or | ||||
| 		// no trailing CRLF on the message body, a CRLF is added.  It makes no | ||||
| 		// other changes to the message body.  In more formal terms, the | ||||
| 		// "simple" body canonicalization algorithm converts "*CRLF" at the end | ||||
| 		// of the body to a single "CRLF". | ||||
| 		// Note that a completely empty or missing body is canonicalized as a | ||||
| 		// single "CRLF"; that is, the canonicalized length will be 2 octets. | ||||
| 		body = bytes.TrimRight(rawBody, "\r\n") | ||||
| 		body = append(body, []byte{13, 10}...) | ||||
| 	} else { | ||||
| 		// relaxed | ||||
| 		// Ignore all whitespace at the end of lines.  Implementations | ||||
| 		// MUST NOT remove the CRLF at the end of the line. | ||||
| 		// Reduce all sequences of WSP within a line to a single SP | ||||
| 		// character. | ||||
| 		// Ignore all empty lines at the end of the message body.  "Empty | ||||
| 		// line" is defined in Section 3.4.3.  If the body is non-empty but | ||||
| 		// does not end with a CRLF, a CRLF is added.  (For email, this is | ||||
| 		// only possible when using extensions to SMTP or non-SMTP transport | ||||
| 		// mechanisms.) | ||||
| 		rawBody = rxReduceWS.ReplaceAll(rawBody, []byte(" ")) | ||||
| 		for _, line := range bytes.SplitAfter(rawBody, []byte{10}) { | ||||
| 			line = bytes.TrimRight(line, " \r\n") | ||||
| 			body = append(body, line...) | ||||
| 			body = append(body, []byte{13, 10}...) | ||||
| 		} | ||||
| 		body = bytes.TrimRight(body, "\r\n") | ||||
| 		body = append(body, []byte{13, 10}...) | ||||
| 
 | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // canonicalizeHeader returns canonicalized version of header | ||||
| func canonicalizeHeader(header string, algo string) (string, error) { | ||||
| 	//rxReduceWS := regexp.MustCompile(`[ \t]+`) | ||||
| 	if algo == "simple" { | ||||
| 		// The "simple" header canonicalization algorithm does not change header | ||||
| 		// fields in any way.  Header fields MUST be presented to the signing or | ||||
| 		// verification algorithm exactly as they are in the message being | ||||
| 		// signed or verified.  In particular, header field names MUST NOT be | ||||
| 		// case folded and whitespace MUST NOT be changed. | ||||
| 		return header, nil | ||||
| 	} else if algo == "relaxed" { | ||||
| 		// The "relaxed" header canonicalization algorithm MUST apply the | ||||
| 		// following steps in order: | ||||
| 
 | ||||
| 		// Convert all header field names (not the header field values) to | ||||
| 		// lowercase.  For example, convert "SUBJect: AbC" to "subject: AbC". | ||||
| 
 | ||||
| 		// Unfold all header field continuation lines as described in | ||||
| 		// [RFC5322]; in particular, lines with terminators embedded in | ||||
| 		// continued header field values (that is, CRLF sequences followed by | ||||
| 		// WSP) MUST be interpreted without the CRLF.  Implementations MUST | ||||
| 		// NOT remove the CRLF at the end of the header field value. | ||||
| 
 | ||||
| 		// Convert all sequences of one or more WSP characters to a single SP | ||||
| 		// character.  WSP characters here include those before and after a | ||||
| 		// line folding boundary. | ||||
| 
 | ||||
| 		// Delete all WSP characters at the end of each unfolded header field | ||||
| 		// value. | ||||
| 
 | ||||
| 		// Delete any WSP characters remaining before and after the colon | ||||
| 		// separating the header field name from the header field value.  The | ||||
| 		// colon separator MUST be retained. | ||||
| 		kv := strings.SplitN(header, ":", 2) | ||||
| 		if len(kv) != 2 { | ||||
| 			return header, ErrBadMailFormatHeaders | ||||
| 		} | ||||
| 		k := strings.ToLower(kv[0]) | ||||
| 		k = strings.TrimSpace(k) | ||||
| 		v := removeFWS(kv[1]) | ||||
| 		//v = rxReduceWS.ReplaceAllString(v, " ") | ||||
| 		//v = strings.TrimSpace(v) | ||||
| 		return k + ":" + v + CRLF, nil | ||||
| 	} | ||||
| 	return header, ErrSignBadCanonicalization | ||||
| } | ||||
| 
 | ||||
| // getBodyHash return the hash (bas64encoded) of the body | ||||
| func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) { | ||||
| 	var h hash.Hash | ||||
| 	if algo == "sha1" { | ||||
| 		h = sha1.New() | ||||
| 	} else { | ||||
| 		h = sha256.New() | ||||
| 	} | ||||
| 	toH := *body | ||||
| 	// if l tag (body length) | ||||
| 	if bodyLength != 0 { | ||||
| 		if uint(len(toH)) < bodyLength { | ||||
| 			return "", ErrBadDKimTagLBodyTooShort | ||||
| 		} | ||||
| 		toH = toH[0:bodyLength] | ||||
| 	} | ||||
| 
 | ||||
| 	h.Write(toH) | ||||
| 	return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil | ||||
| } | ||||
| 
 | ||||
| // getSignature return signature of toSign using key | ||||
| func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, error) { | ||||
| 	var h1 hash.Hash | ||||
| 	var h2 crypto.Hash | ||||
| 	switch algo { | ||||
| 	case "sha1": | ||||
| 		h1 = sha1.New() | ||||
| 		h2 = crypto.SHA1 | ||||
| 		break | ||||
| 	case "sha256": | ||||
| 		h1 = sha256.New() | ||||
| 		h2 = crypto.SHA256 | ||||
| 		break | ||||
| 	default: | ||||
| 		return "", ErrVerifyInappropriateHashAlgo | ||||
| 	} | ||||
| 
 | ||||
| 	// sign | ||||
| 	h1.Write(*toSign) | ||||
| 	sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil)) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return base64.StdEncoding.EncodeToString(sig), nil | ||||
| } | ||||
| 
 | ||||
| // verifySignature verify signature from pubkey | ||||
| func verifySignature(toSign []byte, sig64 string, key *rsa.PublicKey, algo string) error { | ||||
| 	var h1 hash.Hash | ||||
| 	var h2 crypto.Hash | ||||
| 	switch algo { | ||||
| 	case "sha1": | ||||
| 		h1 = sha1.New() | ||||
| 		h2 = crypto.SHA1 | ||||
| 		break | ||||
| 	case "sha256": | ||||
| 		h1 = sha256.New() | ||||
| 		h2 = crypto.SHA256 | ||||
| 		break | ||||
| 	default: | ||||
| 		return ErrVerifyInappropriateHashAlgo | ||||
| 	} | ||||
| 
 | ||||
| 	h1.Write(toSign) | ||||
| 	sig, err := base64.StdEncoding.DecodeString(sig64) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig) | ||||
| } | ||||
| 
 | ||||
| // removeFWS removes all FWS from string | ||||
| func removeFWS(in string) string { | ||||
| 	rxReduceWS := regexp.MustCompile(`[ \t]+`) | ||||
| 	out := strings.Replace(in, "\n", "", -1) | ||||
| 	out = strings.Replace(out, "\r", "", -1) | ||||
| 	out = rxReduceWS.ReplaceAllString(out, " ") | ||||
| 	return strings.TrimSpace(out) | ||||
| } | ||||
| 
 | ||||
| // validateCanonicalization validate canonicalization (c flag) | ||||
| func validateCanonicalization(cano string) (string, error) { | ||||
| 	p := strings.Split(cano, "/") | ||||
| 	if len(p) > 2 { | ||||
| 		return "", ErrSignBadCanonicalization | ||||
| 	} | ||||
| 	if len(p) == 1 { | ||||
| 		cano = cano + "/simple" | ||||
| 	} | ||||
| 	for _, c := range p { | ||||
| 		if c != "simple" && c != "relaxed" { | ||||
| 			return "", ErrSignBadCanonicalization | ||||
| 		} | ||||
| 	} | ||||
| 	return cano, nil | ||||
| } | ||||
| 
 | ||||
| // getHeadersList returns headers as list | ||||
| func getHeadersList(rawHeader *[]byte) (*list.List, error) { | ||||
| 	headersList := list.New() | ||||
| 	currentHeader := []byte{} | ||||
| 	for _, line := range bytes.SplitAfter(*rawHeader, []byte{10}) { | ||||
| 		if line[0] == 32 || line[0] == 9 { | ||||
| 			if len(currentHeader) == 0 { | ||||
| 				return headersList, ErrBadMailFormatHeaders | ||||
| 			} | ||||
| 			currentHeader = append(currentHeader, line...) | ||||
| 		} else { | ||||
| 			// New header, save current if exists | ||||
| 			if len(currentHeader) != 0 { | ||||
| 				headersList.PushBack(string(bytes.TrimRight(currentHeader, "\r\n"))) | ||||
| 				currentHeader = []byte{} | ||||
| 			} | ||||
| 			currentHeader = append(currentHeader, line...) | ||||
| 		} | ||||
| 	} | ||||
| 	headersList.PushBack(string(currentHeader)) | ||||
| 	return headersList, nil | ||||
| } | ||||
| 
 | ||||
| // getHeadersBody return headers and body | ||||
| func getHeadersBody(email *[]byte) ([]byte, []byte, error) { | ||||
| 	substitutedEmail := *email | ||||
| 
 | ||||
| 	// only replace \n with \r\n when \r\n\r\n not exists | ||||
| 	if bytes.Index(*email, []byte{13, 10, 13, 10}) < 0 { | ||||
| 		// \n -> \r\n | ||||
| 		substitutedEmail = bytes.Replace(*email, []byte{10}, []byte{13, 10}, -1) | ||||
| 	} | ||||
| 
 | ||||
| 	parts := bytes.SplitN(substitutedEmail, []byte{13, 10, 13, 10}, 2) | ||||
| 	if len(parts) != 2 { | ||||
| 		return []byte{}, []byte{}, ErrBadMailFormat | ||||
| 	} | ||||
| 	// Empty body | ||||
| 	if len(parts[1]) == 0 { | ||||
| 		parts[1] = []byte{13, 10} | ||||
| 	} | ||||
| 	return parts[0], parts[1], nil | ||||
| } | ||||
							
								
								
									
										545
									
								
								vendor/github.com/toorop/go-dkim/dkimHeader.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										545
									
								
								vendor/github.com/toorop/go-dkim/dkimHeader.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,545 @@ | ||||
| package dkim | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"net/mail" | ||||
| 	"net/textproto" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type dkimHeader struct { | ||||
| 	// Version  This tag defines the version of DKIM | ||||
| 	// specification that applies to the signature record. | ||||
| 	// tag v | ||||
| 	Version string | ||||
| 
 | ||||
| 	// The algorithm used to generate the signature.. | ||||
| 	// Verifiers MUST support "rsa-sha1" and "rsa-sha256"; | ||||
| 	// Signers SHOULD sign using "rsa-sha256". | ||||
| 	// tag a | ||||
| 	Algorithm string | ||||
| 
 | ||||
| 	// The signature data (base64). | ||||
| 	// Whitespace is ignored in this value and MUST be | ||||
| 	// ignored when reassembling the original signature. | ||||
| 	// In particular, the signing process can safely insert | ||||
| 	// FWS in this value in arbitrary places to conform to line-length | ||||
| 	// limits. | ||||
| 	// tag b | ||||
| 	SignatureData string | ||||
| 
 | ||||
| 	// The hash of the canonicalized body part of the message as | ||||
| 	// limited by the "l=" tag (base64; REQUIRED). | ||||
| 	// Whitespace is ignored in this value and MUST be ignored when reassembling the original | ||||
| 	// signature.  In particular, the signing process can safely insert | ||||
| 	// FWS in this value in arbitrary places to conform to line-length | ||||
| 	// limits. | ||||
| 	// tag bh | ||||
| 	BodyHash string | ||||
| 
 | ||||
| 	// Message canonicalization (plain-text; OPTIONAL, default is | ||||
| 	//"simple/simple").  This tag informs the Verifier of the type of | ||||
| 	// canonicalization used to prepare the message for signing.  It | ||||
| 	// consists of two names separated by a "slash" (%d47) character, | ||||
| 	// corresponding to the header and body canonicalization algorithms, | ||||
| 	// respectively.  These algorithms are described in Section 3.4.  If | ||||
| 	// only one algorithm is named, that algorithm is used for the header | ||||
| 	// and "simple" is used for the body.  For example, "c=relaxed" is | ||||
| 	// treated the same as "c=relaxed/simple". | ||||
| 	// tag c | ||||
| 	MessageCanonicalization string | ||||
| 
 | ||||
| 	// The SDID claiming responsibility for an introduction of a message | ||||
| 	//  into the mail stream (plain-text; REQUIRED).  Hence, the SDID | ||||
| 	//  value is used to form the query for the public key.  The SDID MUST | ||||
| 	// correspond to a valid DNS name under which the DKIM key record is | ||||
| 	// published.  The conventions and semantics used by a Signer to | ||||
| 	// create and use a specific SDID are outside the scope of this | ||||
| 	// specification, as is any use of those conventions and semantics. | ||||
| 	// When presented with a signature that does not meet these | ||||
| 	// requirements, Verifiers MUST consider the signature invalid. | ||||
| 	// Internationalized domain names MUST be encoded as A-labels, as | ||||
| 	// described in Section 2.3 of [RFC5890]. | ||||
| 	// tag d | ||||
| 	Domain string | ||||
| 
 | ||||
| 	// Signed header fields (plain-text, but see description; REQUIRED). | ||||
| 	// A colon-separated list of header field names that identify the | ||||
| 	// header fields presented to the signing algorithm.  The field MUST | ||||
| 	// contain the complete list of header fields in the order presented | ||||
| 	// to the signing algorithm.  The field MAY contain names of header | ||||
| 	// fields that do not exist when signed; nonexistent header fields do | ||||
| 	// not contribute to the signature computation (that is, they are | ||||
| 	// treated as the null input, including the header field name, the | ||||
| 	// separating colon, the header field value, and any CRLF | ||||
| 	// terminator).  The field MAY contain multiple instances of a header | ||||
| 	// field name, meaning multiple occurrences of the corresponding | ||||
| 	// header field are included in the header hash.  The field MUST NOT | ||||
| 	// include the DKIM-Signature header field that is being created or | ||||
| 	// verified but may include others.  Folding whitespace (FWS) MAY be | ||||
| 	// included on either side of the colon separator.  Header field | ||||
| 	// names MUST be compared against actual header field names in a | ||||
| 	// case-insensitive manner.  This list MUST NOT be empty.  See | ||||
| 	// Section 5.4 for a discussion of choosing header fields to sign and | ||||
| 	// Section 5.4.2 for requirements when signing multiple instances of | ||||
| 	// a single field. | ||||
| 	// tag h | ||||
| 	Headers []string | ||||
| 
 | ||||
| 	// The Agent or User Identifier (AUID) on behalf of which the SDID is | ||||
| 	// taking responsibility (dkim-quoted-printable; OPTIONAL, default is | ||||
| 	// an empty local-part followed by an "@" followed by the domain from | ||||
| 	// the "d=" tag). | ||||
| 	// The syntax is a standard email address where the local-part MAY be | ||||
| 	// omitted.  The domain part of the address MUST be the same as, or a | ||||
| 	// subdomain of, the value of the "d=" tag. | ||||
| 	// Internationalized domain names MUST be encoded as A-labels, as | ||||
| 	// described in Section 2.3 of [RFC5890]. | ||||
| 	// tag i | ||||
| 	Auid string | ||||
| 
 | ||||
| 	// Body length count (plain-text unsigned decimal integer; OPTIONAL, | ||||
| 	// default is entire body).  This tag informs the Verifier of the | ||||
| 	// number of octets in the body of the email after canonicalization | ||||
| 	// included in the cryptographic hash, starting from 0 immediately | ||||
| 	// following the CRLF preceding the body.  This value MUST NOT be | ||||
| 	// larger than the actual number of octets in the canonicalized | ||||
| 	// message body.  See further discussion in Section 8.2. | ||||
| 	// tag l | ||||
| 	BodyLength uint | ||||
| 
 | ||||
| 	// A colon-separated list of query methods used to retrieve the | ||||
| 	// public key (plain-text; OPTIONAL, default is "dns/txt").  Each | ||||
| 	// query method is of the form "type[/options]", where the syntax and | ||||
| 	// semantics of the options depend on the type and specified options. | ||||
| 	// If there are multiple query mechanisms listed, the choice of query | ||||
| 	// mechanism MUST NOT change the interpretation of the signature. | ||||
| 	// Implementations MUST use the recognized query mechanisms in the | ||||
| 	// order presented.  Unrecognized query mechanisms MUST be ignored. | ||||
| 	// Currently, the only valid value is "dns/txt", which defines the | ||||
| 	// DNS TXT resource record (RR) lookup algorithm described elsewhere | ||||
| 	// in this document.  The only option defined for the "dns" query | ||||
| 	// type is "txt", which MUST be included.  Verifiers and Signers MUST | ||||
| 	// support "dns/txt". | ||||
| 	// tag q | ||||
| 	QueryMethods []string | ||||
| 
 | ||||
| 	// The selector subdividing the namespace for the "d=" (domain) tag | ||||
| 	// (plain-text; REQUIRED). | ||||
| 	// Internationalized selector names MUST be encoded as A-labels, as | ||||
| 	// described in Section 2.3 of [RFC5890]. | ||||
| 	// tag s | ||||
| 	Selector string | ||||
| 
 | ||||
| 	// Signature Timestamp (plain-text unsigned decimal integer; | ||||
| 	// RECOMMENDED, default is an unknown creation time).  The time that | ||||
| 	// this signature was created.  The format is the number of seconds | ||||
| 	// since 00:00:00 on January 1, 1970 in the UTC time zone.  The value | ||||
| 	// is expressed as an unsigned integer in decimal ASCII.  This value | ||||
| 	// is not constrained to fit into a 31- or 32-bit integer. | ||||
| 	// Implementations SHOULD be prepared to handle values up to at least | ||||
| 	// 10^12 (until approximately AD 200,000; this fits into 40 bits). | ||||
| 	// To avoid denial-of-service attacks, implementations MAY consider | ||||
| 	// any value longer than 12 digits to be infinite.  Leap seconds are | ||||
| 	// not counted.  Implementations MAY ignore signatures that have a | ||||
| 	// timestamp in the future. | ||||
| 	// tag t | ||||
| 	SignatureTimestamp time.Time | ||||
| 
 | ||||
| 	// Signature Expiration (plain-text unsigned decimal integer; | ||||
| 	// RECOMMENDED, default is no expiration).  The format is the same as | ||||
| 	// in the "t=" tag, represented as an absolute date, not as a time | ||||
| 	// delta from the signing timestamp.  The value is expressed as an | ||||
| 	// unsigned integer in decimal ASCII, with the same constraints on | ||||
| 	// the value in the "t=" tag.  Signatures MAY be considered invalid | ||||
| 	// if the verification time at the Verifier is past the expiration | ||||
| 	// date.  The verification time should be the time that the message | ||||
| 	// was first received at the administrative domain of the Verifier if | ||||
| 	// that time is reliably available; otherwise, the current time | ||||
| 	// should be used.  The value of the "x=" tag MUST be greater than | ||||
| 	// the value of the "t=" tag if both are present. | ||||
| 	//tag x | ||||
| 	SignatureExpiration time.Time | ||||
| 
 | ||||
| 	// Copied header fields (dkim-quoted-printable, but see description; | ||||
| 	// OPTIONAL, default is null).  A vertical-bar-separated list of | ||||
| 	// selected header fields present when the message was signed, | ||||
| 	// including both the field name and value.  It is not required to | ||||
| 	// include all header fields present at the time of signing.  This | ||||
| 	// field need not contain the same header fields listed in the "h=" | ||||
| 	// tag.  The header field text itself must encode the vertical bar | ||||
| 	// ("|", %x7C) character (i.e., vertical bars in the "z=" text are | ||||
| 	// meta-characters, and any actual vertical bar characters in a | ||||
| 	// copied header field must be encoded).  Note that all whitespace | ||||
| 	// must be encoded, including whitespace between the colon and the | ||||
| 	// header field value.  After encoding, FWS MAY be added at arbitrary | ||||
| 	// locations in order to avoid excessively long lines; such | ||||
| 	// whitespace is NOT part of the value of the header field and MUST | ||||
| 	// be removed before decoding. | ||||
| 	// The header fields referenced by the "h=" tag refer to the fields | ||||
| 	// in the [RFC5322] header of the message, not to any copied fields | ||||
| 	// in the "z=" tag.  Copied header field values are for diagnostic | ||||
| 	// use. | ||||
| 	// tag z | ||||
| 	CopiedHeaderFields []string | ||||
| 
 | ||||
| 	// HeaderMailFromDomain store the raw email address of the header Mail From | ||||
| 	// used for verifying in case of multiple DKIM header (we will prioritise | ||||
| 	// header with d = mail from domain) | ||||
| 	//HeaderMailFromDomain string | ||||
| 
 | ||||
| 	// RawForsign represents the raw part (without canonicalization) of the header | ||||
| 	// used for computint sig in verify process | ||||
| 	RawForSign string | ||||
| } | ||||
| 
 | ||||
| // NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value | ||||
| func newDkimHeaderBySigOptions(options SigOptions) *dkimHeader { | ||||
| 	h := new(dkimHeader) | ||||
| 	h.Version = "1" | ||||
| 	h.Algorithm = options.Algo | ||||
| 	h.MessageCanonicalization = options.Canonicalization | ||||
| 	h.Domain = options.Domain | ||||
| 	h.Headers = options.Headers | ||||
| 	h.Auid = options.Auid | ||||
| 	h.BodyLength = options.BodyLength | ||||
| 	h.QueryMethods = options.QueryMethods | ||||
| 	h.Selector = options.Selector | ||||
| 	if options.AddSignatureTimestamp { | ||||
| 		h.SignatureTimestamp = time.Now() | ||||
| 	} | ||||
| 	if options.SignatureExpireIn > 0 { | ||||
| 		h.SignatureExpiration = time.Now().Add(time.Duration(options.SignatureExpireIn) * time.Second) | ||||
| 	} | ||||
| 	h.CopiedHeaderFields = options.CopiedHeaderFields | ||||
| 	return h | ||||
| } | ||||
| 
 | ||||
| // NewFromEmail return a new DkimHeader by parsing an email | ||||
| // Note: according to RFC 6376 an email can have multiple DKIM Header | ||||
| // in this case we return the last inserted or the last with d== mail from | ||||
| func newDkimHeaderFromEmail(email *[]byte) (*dkimHeader, error) { | ||||
| 	m, err := mail.ReadMessage(bytes.NewReader(*email)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// DKIM header ? | ||||
| 	if len(m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")]) == 0 { | ||||
| 		return nil, ErrDkimHeaderNotFound | ||||
| 	} | ||||
| 
 | ||||
| 	// Get mail from domain | ||||
| 	mailFromDomain := "" | ||||
| 	mailfrom, err := mail.ParseAddress(m.Header.Get(textproto.CanonicalMIMEHeaderKey("From"))) | ||||
| 	if err != nil { | ||||
| 		if err.Error() != "mail: no address" { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		t := strings.SplitAfter(mailfrom.Address, "@") | ||||
| 		if len(t) > 1 { | ||||
| 			mailFromDomain = strings.ToLower(t[1]) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// get raw dkim header | ||||
| 	// we can't use m.header because header key will be converted with textproto.CanonicalMIMEHeaderKey | ||||
| 	// ie if key in header is not DKIM-Signature but Dkim-Signature or DKIM-signature ot... other | ||||
| 	// combination of case, verify will fail. | ||||
| 	rawHeaders, _, err := getHeadersBody(email) | ||||
| 	if err != nil { | ||||
| 		return nil, ErrBadMailFormat | ||||
| 	} | ||||
| 	rawHeadersList, err := getHeadersList(&rawHeaders) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	dkHeaders := []string{} | ||||
| 	for h := rawHeadersList.Front(); h != nil; h = h.Next() { | ||||
| 		if strings.HasPrefix(strings.ToLower(h.Value.(string)), "dkim-signature") { | ||||
| 			dkHeaders = append(dkHeaders, h.Value.(string)) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var keep *dkimHeader | ||||
| 	var keepErr error | ||||
| 	//for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] { | ||||
| 	for _, h := range dkHeaders { | ||||
| 		parsed, err := parseDkHeader(h) | ||||
| 		// if malformed dkim header try next | ||||
| 		if err != nil { | ||||
| 			keepErr = err | ||||
| 			continue | ||||
| 		} | ||||
| 		// Keep first dkim headers | ||||
| 		if keep == nil { | ||||
| 			keep = parsed | ||||
| 		} | ||||
| 		// if d flag == domain keep this header and return | ||||
| 		if mailFromDomain == parsed.Domain { | ||||
| 			return parsed, nil | ||||
| 		} | ||||
| 	} | ||||
| 	if keep == nil { | ||||
| 		return nil, keepErr | ||||
| 	} | ||||
| 	return keep, nil | ||||
| } | ||||
| 
 | ||||
| // parseDkHeader parse raw dkim header | ||||
| func parseDkHeader(header string) (dkh *dkimHeader, err error) { | ||||
| 	dkh = new(dkimHeader) | ||||
| 
 | ||||
| 	keyVal := strings.SplitN(header, ":", 2) | ||||
| 
 | ||||
| 	t := strings.LastIndex(header, "b=") | ||||
| 	if t == -1 { | ||||
| 		return nil, ErrDkimHeaderBTagNotFound | ||||
| 	} | ||||
| 	dkh.RawForSign = header[0 : t+2] | ||||
| 	p := strings.IndexByte(header[t:], ';') | ||||
| 	if p != -1 { | ||||
| 		dkh.RawForSign = dkh.RawForSign + header[t+p:] | ||||
| 	} | ||||
| 
 | ||||
| 	// Mandatory | ||||
| 	mandatoryFlags := make(map[string]bool, 7) //(b'v', b'a', b'b', b'bh', b'd', b'h', b's') | ||||
| 	mandatoryFlags["v"] = false | ||||
| 	mandatoryFlags["a"] = false | ||||
| 	mandatoryFlags["b"] = false | ||||
| 	mandatoryFlags["bh"] = false | ||||
| 	mandatoryFlags["d"] = false | ||||
| 	mandatoryFlags["h"] = false | ||||
| 	mandatoryFlags["s"] = false | ||||
| 
 | ||||
| 	// default values | ||||
| 	dkh.MessageCanonicalization = "simple/simple" | ||||
| 	dkh.QueryMethods = []string{"dns/txt"} | ||||
| 
 | ||||
| 	// unfold && clean | ||||
| 	val := removeFWS(keyVal[1]) | ||||
| 	val = strings.Replace(val, " ", "", -1) | ||||
| 
 | ||||
| 	fs := strings.Split(val, ";") | ||||
| 	for _, f := range fs { | ||||
| 		if f == "" { | ||||
| 			continue | ||||
| 		} | ||||
| 		flagData := strings.SplitN(f, "=", 2) | ||||
| 
 | ||||
| 		// https://github.com/toorop/go-dkim/issues/2 | ||||
| 		// if flag is not in the form key=value (eg doesn't have "=") | ||||
| 		if len(flagData) != 2 { | ||||
| 			return nil, ErrDkimHeaderBadFormat | ||||
| 		} | ||||
| 		flag := strings.ToLower(strings.TrimSpace(flagData[0])) | ||||
| 		data := strings.TrimSpace(flagData[1]) | ||||
| 		switch flag { | ||||
| 		case "v": | ||||
| 			if data != "1" { | ||||
| 				return nil, ErrDkimVersionNotsupported | ||||
| 			} | ||||
| 			dkh.Version = data | ||||
| 			mandatoryFlags["v"] = true | ||||
| 		case "a": | ||||
| 			dkh.Algorithm = strings.ToLower(data) | ||||
| 			if dkh.Algorithm != "rsa-sha1" && dkh.Algorithm != "rsa-sha256" { | ||||
| 				return nil, ErrSignBadAlgo | ||||
| 			} | ||||
| 			mandatoryFlags["a"] = true | ||||
| 		case "b": | ||||
| 			//dkh.SignatureData = removeFWS(data) | ||||
| 			// remove all space | ||||
| 			dkh.SignatureData = strings.Replace(removeFWS(data), " ", "", -1) | ||||
| 			if len(dkh.SignatureData) != 0 { | ||||
| 				mandatoryFlags["b"] = true | ||||
| 			} | ||||
| 		case "bh": | ||||
| 			dkh.BodyHash = removeFWS(data) | ||||
| 			if len(dkh.BodyHash) != 0 { | ||||
| 				mandatoryFlags["bh"] = true | ||||
| 			} | ||||
| 		case "d": | ||||
| 			dkh.Domain = strings.ToLower(data) | ||||
| 			if len(dkh.Domain) != 0 { | ||||
| 				mandatoryFlags["d"] = true | ||||
| 			} | ||||
| 		case "h": | ||||
| 			data = strings.ToLower(data) | ||||
| 			dkh.Headers = strings.Split(data, ":") | ||||
| 			if len(dkh.Headers) != 0 { | ||||
| 				mandatoryFlags["h"] = true | ||||
| 			} | ||||
| 			fromFound := false | ||||
| 			for _, h := range dkh.Headers { | ||||
| 				if h == "from" { | ||||
| 					fromFound = true | ||||
| 				} | ||||
| 			} | ||||
| 			if !fromFound { | ||||
| 				return nil, ErrDkimHeaderNoFromInHTag | ||||
| 			} | ||||
| 		case "s": | ||||
| 			dkh.Selector = strings.ToLower(data) | ||||
| 			if len(dkh.Selector) != 0 { | ||||
| 				mandatoryFlags["s"] = true | ||||
| 			} | ||||
| 		case "c": | ||||
| 			dkh.MessageCanonicalization, err = validateCanonicalization(strings.ToLower(data)) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		case "i": | ||||
| 			if data != "" { | ||||
| 				if !strings.HasSuffix(data, dkh.Domain) { | ||||
| 					return nil, ErrDkimHeaderDomainMismatch | ||||
| 				} | ||||
| 				dkh.Auid = data | ||||
| 			} | ||||
| 		case "l": | ||||
| 			ui, err := strconv.ParseUint(data, 10, 32) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			dkh.BodyLength = uint(ui) | ||||
| 		case "q": | ||||
| 			dkh.QueryMethods = strings.Split(data, ":") | ||||
| 			if len(dkh.QueryMethods) == 0 || strings.ToLower(dkh.QueryMethods[0]) != "dns/txt" { | ||||
| 				return nil, errQueryMethodNotsupported | ||||
| 			} | ||||
| 		case "t": | ||||
| 			ts, err := strconv.ParseInt(data, 10, 64) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			dkh.SignatureTimestamp = time.Unix(ts, 0) | ||||
| 
 | ||||
| 		case "x": | ||||
| 			ts, err := strconv.ParseInt(data, 10, 64) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			dkh.SignatureExpiration = time.Unix(ts, 0) | ||||
| 		case "z": | ||||
| 			dkh.CopiedHeaderFields = strings.Split(data, "|") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// All mandatory flags are in ? | ||||
| 	for _, p := range mandatoryFlags { | ||||
| 		if !p { | ||||
| 			return nil, ErrDkimHeaderMissingRequiredTag | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// default for i/Auid | ||||
| 	if dkh.Auid == "" { | ||||
| 		dkh.Auid = "@" + dkh.Domain | ||||
| 	} | ||||
| 
 | ||||
| 	// defaut for query method | ||||
| 	if len(dkh.QueryMethods) == 0 { | ||||
| 		dkh.QueryMethods = []string{"dns/text"} | ||||
| 	} | ||||
| 
 | ||||
| 	return dkh, nil | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // GetHeaderBase return base header for signers | ||||
| // Todo: some refactoring needed... | ||||
| func (d *dkimHeader) getHeaderBaseForSigning(bodyHash string) string { | ||||
| 	h := "DKIM-Signature: v=" + d.Version + "; a=" + d.Algorithm + "; q=" + strings.Join(d.QueryMethods, ":") + "; c=" + d.MessageCanonicalization + ";" + CRLF + TAB | ||||
| 	subh := "s=" + d.Selector + ";" | ||||
| 	if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength { | ||||
| 		h += subh + FWS | ||||
| 		subh = "" | ||||
| 	} | ||||
| 	subh += " d=" + d.Domain + ";" | ||||
| 
 | ||||
| 	// Auid | ||||
| 	if len(d.Auid) != 0 { | ||||
| 		if len(subh)+len(d.Auid)+4 > MaxHeaderLineLength { | ||||
| 			h += subh + FWS | ||||
| 			subh = "" | ||||
| 		} | ||||
| 		subh += " i=" + d.Auid + ";" | ||||
| 	} | ||||
| 
 | ||||
| 	/*h := "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tmail.io; i=@tmail.io;" + FWS | ||||
| 	subh := "q=dns/txt; s=test;"*/ | ||||
| 
 | ||||
| 	// signature timestamp | ||||
| 	if !d.SignatureTimestamp.IsZero() { | ||||
| 		ts := d.SignatureTimestamp.Unix() | ||||
| 		if len(subh)+14 > MaxHeaderLineLength { | ||||
| 			h += subh + FWS | ||||
| 			subh = "" | ||||
| 		} | ||||
| 		subh += " t=" + fmt.Sprintf("%d", ts) + ";" | ||||
| 	} | ||||
| 	if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength { | ||||
| 		h += subh + FWS | ||||
| 		subh = "" | ||||
| 	} | ||||
| 
 | ||||
| 	// Expiration | ||||
| 	if !d.SignatureExpiration.IsZero() { | ||||
| 		ts := d.SignatureExpiration.Unix() | ||||
| 		if len(subh)+14 > MaxHeaderLineLength { | ||||
| 			h += subh + FWS | ||||
| 			subh = "" | ||||
| 		} | ||||
| 		subh += " x=" + fmt.Sprintf("%d", ts) + ";" | ||||
| 	} | ||||
| 
 | ||||
| 	// body length | ||||
| 	if d.BodyLength != 0 { | ||||
| 		bodyLengthStr := fmt.Sprintf("%d", d.BodyLength) | ||||
| 		if len(subh)+len(bodyLengthStr)+4 > MaxHeaderLineLength { | ||||
| 			h += subh + FWS | ||||
| 			subh = "" | ||||
| 		} | ||||
| 		subh += " l=" + bodyLengthStr + ";" | ||||
| 	} | ||||
| 
 | ||||
| 	// Headers | ||||
| 	if len(subh)+len(d.Headers)+4 > MaxHeaderLineLength { | ||||
| 		h += subh + FWS | ||||
| 		subh = "" | ||||
| 	} | ||||
| 	subh += " h=" | ||||
| 	for _, header := range d.Headers { | ||||
| 		if len(subh)+len(header)+1 > MaxHeaderLineLength { | ||||
| 			h += subh + FWS | ||||
| 			subh = "" | ||||
| 		} | ||||
| 		subh += header + ":" | ||||
| 	} | ||||
| 	subh = subh[:len(subh)-1] + ";" | ||||
| 
 | ||||
| 	// BodyHash | ||||
| 	if len(subh)+5+len(bodyHash) > MaxHeaderLineLength { | ||||
| 		h += subh + FWS | ||||
| 		subh = "" | ||||
| 	} else { | ||||
| 		subh += " " | ||||
| 	} | ||||
| 	subh += "bh=" | ||||
| 	l := len(subh) | ||||
| 	for _, c := range bodyHash { | ||||
| 		subh += string(c) | ||||
| 		l++ | ||||
| 		if l >= MaxHeaderLineLength { | ||||
| 			h += subh + FWS | ||||
| 			subh = "" | ||||
| 			l = 0 | ||||
| 		} | ||||
| 	} | ||||
| 	h += subh + ";" + FWS + "b=" | ||||
| 	return h | ||||
| } | ||||
							
								
								
									
										94
									
								
								vendor/github.com/toorop/go-dkim/errors.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								vendor/github.com/toorop/go-dkim/errors.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,94 @@ | ||||
| package dkim | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	// ErrSignPrivateKeyRequired when there not private key in config | ||||
| 	ErrSignPrivateKeyRequired = errors.New("PrivateKey is required") | ||||
| 
 | ||||
| 	// ErrSignDomainRequired when there is no domain defined in config | ||||
| 	ErrSignDomainRequired = errors.New("Domain is required") | ||||
| 
 | ||||
| 	// ErrSignSelectorRequired when there is no Selcteir defined in config | ||||
| 	ErrSignSelectorRequired = errors.New("Selector is required") | ||||
| 
 | ||||
| 	// ErrSignHeaderShouldContainsFrom If Headers is specified it should at least contain 'from' | ||||
| 	ErrSignHeaderShouldContainsFrom = errors.New("header must contains 'from' field") | ||||
| 
 | ||||
| 	// ErrSignBadCanonicalization If bad Canonicalization parameter | ||||
| 	ErrSignBadCanonicalization = errors.New("bad Canonicalization parameter") | ||||
| 
 | ||||
| 	// ErrCandNotParsePrivateKey when unable to parse private key | ||||
| 	ErrCandNotParsePrivateKey = errors.New("can not parse private key, check format (pem) and validity") | ||||
| 
 | ||||
| 	// ErrSignBadAlgo Bad algorithm | ||||
| 	ErrSignBadAlgo = errors.New("bad algorithm. Only rsa-sha1 or rsa-sha256 are permitted") | ||||
| 
 | ||||
| 	// ErrBadMailFormat unable to parse mail | ||||
| 	ErrBadMailFormat = errors.New("bad mail format") | ||||
| 
 | ||||
| 	// ErrBadMailFormatHeaders bad headers format (not DKIM Header) | ||||
| 	ErrBadMailFormatHeaders = errors.New("bad mail format found in headers") | ||||
| 
 | ||||
| 	// ErrBadDKimTagLBodyTooShort bad l tag | ||||
| 	ErrBadDKimTagLBodyTooShort = errors.New("bad tag l or bodyLength option. Body length < l value") | ||||
| 
 | ||||
| 	// ErrDkimHeaderBadFormat when errors found in DKIM header | ||||
| 	ErrDkimHeaderBadFormat = errors.New("bad DKIM header format") | ||||
| 
 | ||||
| 	// ErrDkimHeaderNotFound when there's no DKIM-Signature header in an email we have to verify | ||||
| 	ErrDkimHeaderNotFound = errors.New("no DKIM-Signature header field found ") | ||||
| 
 | ||||
| 	// ErrDkimHeaderBTagNotFound when there's no b tag | ||||
| 	ErrDkimHeaderBTagNotFound = errors.New("no tag 'b' found in dkim header") | ||||
| 
 | ||||
| 	// ErrDkimHeaderNoFromInHTag when from is missing in h tag | ||||
| 	ErrDkimHeaderNoFromInHTag = errors.New("'from' header is missing in h tag") | ||||
| 
 | ||||
| 	// ErrDkimHeaderMissingRequiredTag when a required tag is missing | ||||
| 	ErrDkimHeaderMissingRequiredTag = errors.New("signature missing required tag") | ||||
| 
 | ||||
| 	// ErrDkimHeaderDomainMismatch if i tag is not a sub domain of d tag | ||||
| 	ErrDkimHeaderDomainMismatch = errors.New("domain mismatch") | ||||
| 
 | ||||
| 	// ErrDkimVersionNotsupported version not supported | ||||
| 	ErrDkimVersionNotsupported = errors.New("incompatible version") | ||||
| 
 | ||||
| 	// Query method unsupported | ||||
| 	errQueryMethodNotsupported = errors.New("query method not supported") | ||||
| 
 | ||||
| 	// ErrVerifyBodyHash when body hash doesn't verify | ||||
| 	ErrVerifyBodyHash = errors.New("body hash did not verify") | ||||
| 
 | ||||
| 	// ErrVerifyNoKeyForSignature no key | ||||
| 	ErrVerifyNoKeyForSignature = errors.New("no key for verify") | ||||
| 
 | ||||
| 	// ErrVerifyKeyUnavailable when service (dns) is anavailable | ||||
| 	ErrVerifyKeyUnavailable = errors.New("key unavailable") | ||||
| 
 | ||||
| 	// ErrVerifyTagVMustBeTheFirst if present the v tag must be the firts in the record | ||||
| 	ErrVerifyTagVMustBeTheFirst = errors.New("pub key syntax error: v tag must be the first") | ||||
| 
 | ||||
| 	// ErrVerifyVersionMusBeDkim1 if présent flag v (version) must be DKIM1 | ||||
| 	ErrVerifyVersionMusBeDkim1 = errors.New("flag v must be set to DKIM1") | ||||
| 
 | ||||
| 	// ErrVerifyBadKeyType bad type for pub key (only rsa is accepted) | ||||
| 	ErrVerifyBadKeyType = errors.New("bad type for key type") | ||||
| 
 | ||||
| 	// ErrVerifyRevokedKey key(s) for this selector is revoked (p is empty) | ||||
| 	ErrVerifyRevokedKey = errors.New("revoked key") | ||||
| 
 | ||||
| 	// ErrVerifyBadKey when we can't parse pubkey | ||||
| 	ErrVerifyBadKey = errors.New("unable to parse pub key") | ||||
| 
 | ||||
| 	// ErrVerifyNoKey when no key is found on DNS record | ||||
| 	ErrVerifyNoKey = errors.New("no public key found in DNS TXT") | ||||
| 
 | ||||
| 	// ErrVerifySignatureHasExpired when signature has expired | ||||
| 	ErrVerifySignatureHasExpired = errors.New("signature has expired") | ||||
| 
 | ||||
| 	// ErrVerifyInappropriateHashAlgo when h tag in pub key doesn't contain hash algo from a tag of DKIM header | ||||
| 	ErrVerifyInappropriateHashAlgo = errors.New("inappropriate has algorithm") | ||||
| ) | ||||
							
								
								
									
										181
									
								
								vendor/github.com/toorop/go-dkim/pubKeyRep.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								vendor/github.com/toorop/go-dkim/pubKeyRep.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,181 @@ | ||||
| package dkim | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/base64" | ||||
| 	"io/ioutil" | ||||
| 	"mime/quotedprintable" | ||||
| 	"net" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // PubKeyRep represents a parsed version of public key record | ||||
| type PubKeyRep struct { | ||||
| 	Version      string | ||||
| 	HashAlgo     []string | ||||
| 	KeyType      string | ||||
| 	Note         string | ||||
| 	PubKey       rsa.PublicKey | ||||
| 	ServiceType  []string | ||||
| 	FlagTesting  bool // flag y | ||||
| 	FlagIMustBeD bool // flag i | ||||
| } | ||||
| 
 | ||||
| // DNSOptions holds settings for looking up DNS records | ||||
| type DNSOptions struct { | ||||
| 	netLookupTXT func(name string) ([]string, error) | ||||
| } | ||||
| 
 | ||||
| // DNSOpt represents an optional setting for looking up DNS records | ||||
| type DNSOpt interface { | ||||
| 	apply(*DNSOptions) | ||||
| } | ||||
| 
 | ||||
| type dnsOpt func(*DNSOptions) | ||||
| 
 | ||||
| func (opt dnsOpt) apply(dnsOpts *DNSOptions) { | ||||
| 	opt(dnsOpts) | ||||
| } | ||||
| 
 | ||||
| // DNSOptLookupTXT sets the function to use to lookup TXT records. | ||||
| // | ||||
| // This should probably only be used in tests. | ||||
| func DNSOptLookupTXT(netLookupTXT func(name string) ([]string, error)) DNSOpt { | ||||
| 	return dnsOpt(func(opts *DNSOptions) { | ||||
| 		opts.netLookupTXT = netLookupTXT | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // NewPubKeyRespFromDNS retrieves the TXT record from DNS based on the specified domain and selector | ||||
| // and parses it. | ||||
| func NewPubKeyRespFromDNS(selector, domain string, opts ...DNSOpt) (*PubKeyRep, verifyOutput, error) { | ||||
| 	dnsOpts := DNSOptions{} | ||||
| 
 | ||||
| 	for _, opt := range opts { | ||||
| 		opt.apply(&dnsOpts) | ||||
| 	} | ||||
| 
 | ||||
| 	if dnsOpts.netLookupTXT == nil { | ||||
| 		dnsOpts.netLookupTXT = net.LookupTXT | ||||
| 	} | ||||
| 
 | ||||
| 	txt, err := dnsOpts.netLookupTXT(selector + "._domainkey." + domain) | ||||
| 	if err != nil { | ||||
| 		if strings.HasSuffix(err.Error(), "no such host") { | ||||
| 			return nil, PERMFAIL, ErrVerifyNoKeyForSignature | ||||
| 		} | ||||
| 
 | ||||
| 		return nil, TEMPFAIL, ErrVerifyKeyUnavailable | ||||
| 	} | ||||
| 
 | ||||
| 	// empty record | ||||
| 	if len(txt) == 0 { | ||||
| 		return nil, PERMFAIL, ErrVerifyNoKeyForSignature | ||||
| 	} | ||||
| 
 | ||||
| 	// parsing, we keep the first record | ||||
| 	// TODO: if there is multiple record | ||||
| 
 | ||||
| 	return NewPubKeyResp(txt[0]) | ||||
| } | ||||
| 
 | ||||
| // NewPubKeyResp parses DKIM record (usually from DNS) | ||||
| func NewPubKeyResp(dkimRecord string) (*PubKeyRep, verifyOutput, error) { | ||||
| 	pkr := new(PubKeyRep) | ||||
| 	pkr.Version = "DKIM1" | ||||
| 	pkr.HashAlgo = []string{"sha1", "sha256"} | ||||
| 	pkr.KeyType = "rsa" | ||||
| 	pkr.FlagTesting = false | ||||
| 	pkr.FlagIMustBeD = false | ||||
| 
 | ||||
| 	p := strings.Split(dkimRecord, ";") | ||||
| 	for i, data := range p { | ||||
| 		keyVal := strings.SplitN(data, "=", 2) | ||||
| 		val := "" | ||||
| 		if len(keyVal) > 1 { | ||||
| 			val = strings.TrimSpace(keyVal[1]) | ||||
| 		} | ||||
| 		switch strings.ToLower(strings.TrimSpace(keyVal[0])) { | ||||
| 		case "v": | ||||
| 			// RFC: is this tag is specified it MUST be the first in the record | ||||
| 			if i != 0 { | ||||
| 				return nil, PERMFAIL, ErrVerifyTagVMustBeTheFirst | ||||
| 			} | ||||
| 			pkr.Version = val | ||||
| 			if pkr.Version != "DKIM1" { | ||||
| 				return nil, PERMFAIL, ErrVerifyVersionMusBeDkim1 | ||||
| 			} | ||||
| 		case "h": | ||||
| 			p := strings.Split(strings.ToLower(val), ":") | ||||
| 			pkr.HashAlgo = []string{} | ||||
| 			for _, h := range p { | ||||
| 				h = strings.TrimSpace(h) | ||||
| 				if h == "sha1" || h == "sha256" { | ||||
| 					pkr.HashAlgo = append(pkr.HashAlgo, h) | ||||
| 				} | ||||
| 			} | ||||
| 			// if empty switch back to default | ||||
| 			if len(pkr.HashAlgo) == 0 { | ||||
| 				pkr.HashAlgo = []string{"sha1", "sha256"} | ||||
| 			} | ||||
| 		case "k": | ||||
| 			if strings.ToLower(val) != "rsa" { | ||||
| 				return nil, PERMFAIL, ErrVerifyBadKeyType | ||||
| 			} | ||||
| 		case "n": | ||||
| 			qp, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(val))) | ||||
| 			if err == nil { | ||||
| 				val = string(qp) | ||||
| 			} | ||||
| 			pkr.Note = val | ||||
| 		case "p": | ||||
| 			rawkey := val | ||||
| 			if rawkey == "" { | ||||
| 				return nil, PERMFAIL, ErrVerifyRevokedKey | ||||
| 			} | ||||
| 			un64, err := base64.StdEncoding.DecodeString(rawkey) | ||||
| 			if err != nil { | ||||
| 				return nil, PERMFAIL, ErrVerifyBadKey | ||||
| 			} | ||||
| 			pk, err := x509.ParsePKIXPublicKey(un64) | ||||
| 			if pk, ok := pk.(*rsa.PublicKey); ok { | ||||
| 				pkr.PubKey = *pk | ||||
| 			} | ||||
| 		case "s": | ||||
| 			t := strings.Split(strings.ToLower(val), ":") | ||||
| 			for _, tt := range t { | ||||
| 				tt = strings.TrimSpace(tt) | ||||
| 				switch tt { | ||||
| 				case "*": | ||||
| 					pkr.ServiceType = append(pkr.ServiceType, "all") | ||||
| 				case "email": | ||||
| 					pkr.ServiceType = append(pkr.ServiceType, tt) | ||||
| 				} | ||||
| 			} | ||||
| 		case "t": | ||||
| 			flags := strings.Split(strings.ToLower(val), ":") | ||||
| 			for _, flag := range flags { | ||||
| 				flag = strings.TrimSpace(flag) | ||||
| 				switch flag { | ||||
| 				case "y": | ||||
| 					pkr.FlagTesting = true | ||||
| 				case "s": | ||||
| 					pkr.FlagIMustBeD = true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// if no pubkey | ||||
| 	if pkr.PubKey == (rsa.PublicKey{}) { | ||||
| 		return nil, PERMFAIL, ErrVerifyNoKey | ||||
| 	} | ||||
| 
 | ||||
| 	// No service type | ||||
| 	if len(pkr.ServiceType) == 0 { | ||||
| 		pkr.ServiceType = []string{"all"} | ||||
| 	} | ||||
| 
 | ||||
| 	return pkr, SUCCESS, nil | ||||
| } | ||||
							
								
								
									
										4
									
								
								vendor/github.com/toorop/go-dkim/watch
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								vendor/github.com/toorop/go-dkim/watch
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| while true | ||||
| do  | ||||
| inotifywait -q -r -e modify,attrib,close_write,move,create,delete . && echo "--------------" && go test -v | ||||
| done | ||||
							
								
								
									
										3
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							| @ -59,6 +59,9 @@ github.com/tidwall/rtree | ||||
| github.com/tidwall/rtree/base | ||||
| # github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 | ||||
| github.com/tidwall/tinyqueue | ||||
| # github.com/toorop/go-dkim v0.0.0-20191019073156-897ad64a2eeb | ||||
| ## explicit | ||||
| github.com/toorop/go-dkim | ||||
| # golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 | ||||
| ## explicit | ||||
| golang.org/x/crypto/bcrypt | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Shivaram Lingamneni
						Shivaram Lingamneni