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