From c27b209a796d45c6c50bec004033ee5c81a550b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Depierrepont=20aka=20Toorop?= Date: Tue, 5 May 2015 12:12:57 +0200 Subject: [PATCH] sign implemted (need real life testing) --- dkim.go | 193 +++++++++++++++++++++++++++++++++++++------------- dkimHeader.go | 98 +++++++++++++++++++++++++ dkim_test.go | 27 +++++-- 3 files changed, 263 insertions(+), 55 deletions(-) diff --git a/dkim.go b/dkim.go index f4c5184..12d4b88 100644 --- a/dkim.go +++ b/dkim.go @@ -3,6 +3,15 @@ package dkim import ( "bytes" "container/list" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "hash" "io/ioutil" "regexp" "strings" @@ -10,7 +19,10 @@ import ( ) const ( - CRLF = "\r\n" + CRLF = "\r\n" + TAB = "\t" + FWS = CRLF + TAB + MaxHeaderLineLength = 70 ) // sigOptions represents signing options @@ -54,6 +66,9 @@ type sigOptions struct { // Time validity of the signature (0=never) SignatureExpireIn time.Duration + + // CopiedHeaderFileds + CopiedHeaderFileds []string } // NewSigOption returns new sigoption with some defaults value @@ -72,6 +87,7 @@ func NewSigOptions() sigOptions { // Sign signs an email func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { + var privateKey *rsa.PrivateKey // check && sanitize config // PrivateKey (required & TODO: valid) @@ -79,6 +95,13 @@ func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { return nil, ErrSignPrivateKeyRequired } + d, _ := pem.Decode([]byte(options.PrivateKey)) + key, err := x509.ParsePKCS1PrivateKey(d.Bytes) + if err != nil { + return nil, err + } + privateKey = key + // Domain required if options.Domain == "" { return nil, ErrSignDomainRequired @@ -125,13 +148,74 @@ func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { } // Normalize - //normalizedHeaders, NormalizedBody, err := normalize(email, options) + headers, body, err := canonicalize(email, options) + if err != nil { + return nil, err + } - canonicalize(email, options) + // hash body + var bodyHash string + var h1, h2 hash.Hash + var h3 crypto.Hash + signHash := strings.Split(options.Algo, "-") + if signHash[1] == "sha1" { + h1 = sha1.New() + h2 = sha1.New() + h3 = crypto.SHA1 + } else { + h1 = sha256.New() + h2 = sha256.New() + h3 = crypto.SHA256 + } + bodyHash = base64.StdEncoding.EncodeToString(h1.Sum(body)) - return nil, nil + // Get dkim header base + dkimHeader := NewDkimHeaderBySigOptions(options) + dHeader := dkimHeader.GetHeaderBase(bodyHash) + + canonicalizations := strings.Split(options.Canonicalization, "/") + dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) + if err != nil { + return nil, err + } + headers = append(headers, []byte(dHeaderCanonicalized)...) + + // sign + h2.Write(headers) + sig, err := rsa.SignPKCS1v15(rand.Reader, privateKey, h3, h2.Sum(nil)) + if err != nil { + return nil, err + } + sig64 := base64.StdEncoding.EncodeToString(sig) + + // add to DKIM-Header + + dHeader += ";" + FWS + subh := "b=" + l := len(subh) + for _, c := range sig64 { + subh += string(c) + l++ + if l >= MaxHeaderLineLength { + dHeader += subh + FWS + subh = "" + l = 0 + } + } + dHeader += subh + CRLF + + // Out + rawmail := []byte(dHeader) + t, err := ioutil.ReadAll(email) + if err != nil { + return nil, err + } + + rawmail = append(rawmail, t...) + return bytes.NewReader(rawmail), nil } +// canonicalize returns canonicalized version of header and body func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body []byte, err error) { var email []byte body = []byte{} @@ -157,8 +241,6 @@ 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}) { @@ -170,7 +252,6 @@ func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body } else { // New header, save current if exists if len(currentHeader) != 0 { - //headersMap = append(headersMap, currentHeader) headersList.PushBack(string(currentHeader)) currentHeader = []byte{} @@ -201,51 +282,13 @@ func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body } } - 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)...) + //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" { @@ -290,3 +333,51 @@ func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body println(string(body))*/ 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 := strings.Replace(kv[1], "\n", "", -1) + v = strings.Replace(v, "\r", "", -1) + v = rxReduceWS.ReplaceAllString(v, " ") + v = strings.TrimSpace(v) + return k + ":" + v + CRLF, nil + } + return header, ErrSignBadCanonicalization +} diff --git a/dkimHeader.go b/dkimHeader.go index 6ed4920..750ad4d 100644 --- a/dkimHeader.go +++ b/dkimHeader.go @@ -1,6 +1,8 @@ package dkim import ( + "fmt" + "strings" "time" ) @@ -180,3 +182,99 @@ type DkimHeader struct { // tag z CopiedHeaderFileds []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.Seconds() > 0 { + h.SignatureExpiration = time.Now().Add(options.SignatureExpireIn) + } + h.CopiedHeaderFileds = options.CopiedHeaderFileds + return h +} + +// GetHeaderBase return base header for signers +// Todo: some refactoring... +func (d *DkimHeader) GetHeaderBase(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 + ";" + + // 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) + ";" + + } + + // 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 + + return h +} diff --git a/dkim_test.go b/dkim_test.go index 212ee78..7ae25af 100644 --- a/dkim_test.go +++ b/dkim_test.go @@ -2,12 +2,15 @@ package dkim import ( "bytes" + "fmt" "github.com/stretchr/testify/assert" + "io/ioutil" "testing" ) const ( - privKey = `MIICXQIBAAKBgQDNUXO+Qsl1tw+GjrqFajz0ERSEUs1FHSL/+udZRWn1Atw8gz0+ + privKey = `-----BEGIN RSA PRIVATE KEY----- + MIICXQIBAAKBgQDNUXO+Qsl1tw+GjrqFajz0ERSEUs1FHSL/+udZRWn1Atw8gz0+ tcGqhWChBDeU9gY5sKLEAZnX3FjC/T/IbqeiSM68kS5vLkzRI84eiJrm3+IieUqI IicsO+WYxQs+JgVx5XhpPjX4SQjHtwEC2xKkWnEv+VPgO1JWdooURcSC6QIDAQAB AoGAM9exRgVPIS4L+Ynohu+AXJBDgfX2ZtEomUIdUGk6i+cg/RaWTFNQh2IOOBn8 @@ -19,7 +22,8 @@ uGhq0DPojmhsmUC8jUeLe79CllZNP3LU1wJBAIZcoCnI7g5Bcdr4nyxfJ4pkw4cQ S4FT0XAZPR/YZrADo8/SWCWPdFTGSuaf17nL6vLD1zljK/skY5LwshrvUCMCQQDM MY7ehj6DVFHYlt2LFSyhInCZscTencgK24KfGF5t1JZlwt34YaMqjAMACmi/55Fc e7DIxW5nI/nDZrOY+EAjAkA3BHUx3PeXkXJnXjlh7nGZmk/v8tB5fiofAwfXNfL7 -bz0ZrT2Caz995Dpjommh5aMpCJvUGsrYCG6/Pbha9NXl` +bz0ZrT2Caz995Dpjommh5aMpCJvUGsrYCG6/Pbha9NXl +-----END RSA PRIVATE KEY-----` pubKey = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNUXO+Qsl1tw+GjrqFajz0ERSE Us1FHSL/+udZRWn1Atw8gz0+tcGqhWChBDeU9gY5sKLEAZnX3FjC/T/IbqeiSM68 @@ -90,45 +94,55 @@ func Test_SignConfig(t *testing.T) { assert.NotNil(t, err) // && err No private key assert.EqualError(t, err, ErrSignPrivateKeyRequired.Error()) - options.PrivateKey = "toto" + options.PrivateKey = privKey _, err = Sign(emailReader, options) + emailReader.Seek(0, 0) // Domain assert.EqualError(t, err, ErrSignDomainRequired.Error()) options.Domain = "toorop.fr" _, err = Sign(emailReader, options) + emailReader.Seek(0, 0) // Selector assert.Error(t, err, ErrSignSelectorRequired.Error()) options.Selector = "default" _, err = Sign(emailReader, options) assert.NoError(t, err) + emailReader.Seek(0, 0) // Canonicalization options.Canonicalization = "simple/relaxed/simple" _, err = Sign(emailReader, options) assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) + emailReader.Seek(0, 0) options.Canonicalization = "simple/relax" _, err = Sign(emailReader, options) + emailReader.Seek(0, 0) assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) + emailReader.Seek(0, 0) options.Canonicalization = "relaxed" _, err = Sign(emailReader, options) assert.NoError(t, err) + emailReader.Seek(0, 0) options.Canonicalization = "SiMple/relAxed" _, err = Sign(emailReader, options) assert.NoError(t, err) + emailReader.Seek(0, 0) // header options.Headers = []string{"toto"} _, err = Sign(emailReader, options) assert.EqualError(t, err, ErrSignHeaderShouldContainsFrom.Error()) + emailReader.Seek(0, 0) options.Headers = []string{"To", "From"} _, err = Sign(emailReader, options) assert.NoError(t, err) + emailReader.Seek(0, 0) } func Test_canonicalize(t *testing.T) { @@ -155,9 +169,14 @@ func Test_Sign(t *testing.T) { emailReader := bytes.NewReader([]byte(email)) options := NewSigOptions() options.PrivateKey = privKey - options.Canonicalization = "simple/relaxed" + options.Canonicalization = "relaxed/relaxed" options.Domain = domain options.Selector = selector + options.AddSignatureTimestamp = true + options.SignatureExpireIn = 3600 + options.Headers = []string{"from", "date", "mime-version", "received", "received", "In-Reply-To"} emailReader, err := Sign(emailReader, options) assert.NoError(t, err) + raw, _ := ioutil.ReadAll(emailReader) + fmt.Println(string(raw)) }