diff --git a/dkim.go b/dkim.go index 170a68f..f4c5184 100644 --- a/dkim.go +++ b/dkim.go @@ -2,7 +2,7 @@ package dkim import ( "bytes" - //"fmt" + "container/list" "io/ioutil" "regexp" "strings" @@ -155,6 +155,98 @@ func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body } canonicalizations := strings.Split(options.Canonicalization, "/") + + // canonicalyze header + //var headersMap [][]byte + //headersMap := [][]byte{} + headersList := list.New() + currentHeader := []byte{} + for _, line := range bytes.SplitAfter(parts[0], []byte{10}) { + if line[0] == 32 || line[0] == 9 { + if len(currentHeader) == 0 { + return headers, body, ErrBadMailFormatHeaders + } + currentHeader = append(currentHeader, line...) + } else { + // New header, save current if exists + if len(currentHeader) != 0 { + //headersMap = append(headersMap, currentHeader) + headersList.PushBack(string(currentHeader)) + currentHeader = []byte{} + + } + currentHeader = append(currentHeader, line...) + } + } + + // 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 options.Headers { + match = nil + headerToKeepToLower := strings.ToLower(headerToKeep) + for e := headersList.Front(); e != nil; e = e.Next() { + t := strings.Split(e.Value.(string), ":") + if strings.ToLower(t[0]) == headerToKeepToLower { + match = e + } + } + if match != nil { + headersToKeepList.PushBack(match.Value.(string)) + headersList.Remove(match) + } else { + headersToKeepList.PushBack(headerToKeep + ":\r\n") + } + } + + if canonicalizations[0] == "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. + for e := headersToKeepList.Front(); e != nil; e = e.Next() { + headers = append(headers, []byte(e.Value.(string))...) + } + } else { + // 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. + for e := headersToKeepList.Front(); e != nil; e = e.Next() { + kv := strings.SplitN(e.Value.(string), ":", 2) + if len(kv) != 2 { + return []byte{}, []byte{}, ErrBadMailFormatHeaders + } + k := strings.ToLower(kv[0]) + k = strings.TrimSpace(k) + v := strings.Replace(kv[1], "\n", "", -1) + v = strings.Replace(v, "\r", "", -1) + v = rxReduceWS.ReplaceAllString(v, " ") + v = strings.TrimSpace(v) + headers = append(headers, []byte(k+":"+v+CRLF)...) + } + } // canonicalyze body if canonicalizations[1] == "simple" { // simple diff --git a/dkim_test.go b/dkim_test.go index 9af4899..212ee78 100644 --- a/dkim_test.go +++ b/dkim_test.go @@ -33,6 +33,9 @@ kS5vLkzRI84eiJrm3+IieUqIIicsO+WYxQs+JgVx5XhpPjX4SQjHtwEC2xKkWnEv var email = "Received: (qmail 28277 invoked from network); 1 May 2015 09:43:37 -0000" + CRLF + "Received: (qmail 21323 invoked from network); 1 May 2015 09:48:39 -0000" + CRLF + + "Received: from mail483.ha.ovh.net (b6.ovh.net [213.186.33.56])" + CRLF + + " by mo51.mail-out.ovh.net (Postfix) with SMTP id A6E22FF8934" + CRLF + + " for ; Mon, 4 May 2015 14:00:47 +0200 (CEST)" + CRLF + "MIME-Version: 1.0" + CRLF + "Date: Fri, 1 May 2015 11:48:37 +0200" + CRLF + "Message-ID: " + CRLF + @@ -46,6 +49,22 @@ var email = "Received: (qmail 28277 invoked from network); 1 May 2015 09:43:37 - "-- " + CRLF + "Toorop " + CRLF + CRLF + CRLF + CRLF + CRLF + CRLF +var headerSimple = "From: =?UTF-8?Q?St=C3=A9phane_Depierrepont?= " + CRLF + + "Date: Fri, 1 May 2015 11:48:37 +0200" + CRLF + + "MIME-Version: 1.0" + CRLF + + "Received: from mail483.ha.ovh.net (b6.ovh.net [213.186.33.56])" + CRLF + + " by mo51.mail-out.ovh.net (Postfix) with SMTP id A6E22FF8934" + CRLF + + " for ; Mon, 4 May 2015 14:00:47 +0200 (CEST)" + CRLF + + "Received: (qmail 21323 invoked from network); 1 May 2015 09:48:39 -0000" + CRLF + + "In-Reply-To:" + CRLF + +var headerRelaxed = "from:=?UTF-8?Q?St=C3=A9phane_Depierrepont?= " + CRLF + + "date:Fri, 1 May 2015 11:48:37 +0200" + CRLF + + "mime-version:1.0" + CRLF + + "received:from mail483.ha.ovh.net (b6.ovh.net [213.186.33.56]) by mo51.mail-out.ovh.net (Postfix) with SMTP id A6E22FF8934 for ; Mon, 4 May 2015 14:00:47 +0200 (CEST)" + CRLF + + "received:(qmail 21323 invoked from network); 1 May 2015 09:48:39 -0000" + CRLF + + "in-reply-to:" + CRLF + var bodySimple = "Hello world" + CRLF + "line with trailing space " + CRLF + "line with space " + CRLF + @@ -112,6 +131,26 @@ func Test_SignConfig(t *testing.T) { assert.NoError(t, err) } +func Test_canonicalize(t *testing.T) { + emailReader := bytes.NewReader([]byte(email)) + options := NewSigOptions() + options.Headers = []string{"from", "date", "mime-version", "received", "received", "In-Reply-To"} + // simple/simple + options.Canonicalization = "simple/simple" + header, body, err := canonicalize(emailReader, options) + assert.NoError(t, err) + assert.Equal(t, []byte(headerSimple), header) + assert.Equal(t, []byte(bodySimple), body) + + // relaxed/relaxed + options.Canonicalization = "relaxed/relaxed" + header, body, err = canonicalize(emailReader, options) + assert.NoError(t, err) + assert.Equal(t, []byte(headerRelaxed), header) + assert.Equal(t, []byte(bodyRelaxed), body) + +} + func Test_Sign(t *testing.T) { emailReader := bytes.NewReader([]byte(email)) options := NewSigOptions() @@ -122,20 +161,3 @@ func Test_Sign(t *testing.T) { emailReader, err := Sign(emailReader, options) assert.NoError(t, err) } - -func Test_canonicalize(t *testing.T) { - emailReader := bytes.NewReader([]byte(email)) - options := NewSigOptions() - // simple/simple - options.Canonicalization = "simple/simple" - _, body, err := canonicalize(emailReader, options) - assert.NoError(t, err) - assert.Equal(t, []byte(bodySimple), body) - - // relaxed/relaxed - options.Canonicalization = "relaxed/relaxed" - _, body, err = canonicalize(emailReader, options) - assert.NoError(t, err) - assert.Equal(t, []byte(bodyRelaxed), body) - -} diff --git a/errors.go b/errors.go index 4e18de3..c2094c2 100644 --- a/errors.go +++ b/errors.go @@ -24,5 +24,8 @@ var ( ErrSignBadAlgo = errors.New("bad algorithm. Only rsa-sha1 or rsa-sha256 are permitted") // ErrBadMailFormat - ErrBadMailFormat = errors.New("bad mail formart") + ErrBadMailFormat = errors.New("bad mail format") + + // ErrBadMailFormatHeaders + ErrBadMailFormatHeaders = errors.New("bad mail format found in headers") )