diff --git a/dkim.go b/dkim.go index 6c0f061..da7518c 100644 --- a/dkim.go +++ b/dkim.go @@ -11,8 +11,9 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + //"fmt" "hash" - "io/ioutil" + //"io/ioutil" "regexp" "strings" ) @@ -74,61 +75,60 @@ type sigOptions struct { func NewSigOptions() sigOptions { return sigOptions{ Version: 1, - Canonicalization: "simple/simple", + Canonicalization: "relaxed/simple", Algo: "rsa-sha256", Headers: []string{"from"}, BodyLength: 0, QueryMethods: []string{"dns/txt"}, - AddSignatureTimestamp: false, + AddSignatureTimestamp: true, SignatureExpireIn: 0, } } // Sign signs an email -func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { +func Sign(email *[]byte, options sigOptions) error { var privateKey *rsa.PrivateKey - // PrivateKey (required & TODO: valid) + // PrivateKey if options.PrivateKey == "" { - return nil, ErrSignPrivateKeyRequired + return ErrSignPrivateKeyRequired } - d, _ := pem.Decode([]byte(options.PrivateKey)) key, err := x509.ParsePKCS1PrivateKey(d.Bytes) if err != nil { - return nil, ErrCandNotParsePrivateKey + return ErrCandNotParsePrivateKey } privateKey = key // Domain required if options.Domain == "" { - return nil, ErrSignDomainRequired + return ErrSignDomainRequired } // Selector required if options.Selector == "" { - return nil, ErrSignSelectorRequired + return ErrSignSelectorRequired } // Canonicalization options.Canonicalization = strings.ToLower(options.Canonicalization) p := strings.Split(options.Canonicalization, "/") if len(p) > 2 { - return nil, ErrSignBadCanonicalization + return ErrSignBadCanonicalization } if len(p) == 1 { options.Canonicalization = options.Canonicalization + "/simple" } for _, c := range p { if c != "simple" && c != "relaxed" { - return nil, ErrSignBadCanonicalization + return ErrSignBadCanonicalization } } // Algo options.Algo = strings.ToLower(options.Algo) if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" { - return nil, ErrSignBadAlgo + return ErrSignBadAlgo } // Header must contain "from" @@ -142,13 +142,13 @@ func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { } } if !hasFrom { - return nil, ErrSignHeaderShouldContainsFrom + return ErrSignHeaderShouldContainsFrom } // Normalize headers, body, err := canonicalize(email, options) if err != nil { - return nil, err + return err } // hash body @@ -169,7 +169,7 @@ func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { // if l tag (body length) if options.BodyLength != 0 { if uint(len(body)) < options.BodyLength { - return nil, ErrBadDKimTagLBodyTooShort + return ErrBadDKimTagLBodyTooShort } body = body[0:options.BodyLength] } @@ -184,7 +184,7 @@ func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { canonicalizations := strings.Split(options.Canonicalization, "/") dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) if err != nil { - return nil, err + return err } headers = append(headers, []byte(dHeaderCanonicalized)...) headers = bytes.TrimRight(headers, " \r\n") @@ -193,7 +193,7 @@ func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { h2.Write(headers) sig, err := rsa.SignPKCS1v15(rand.Reader, privateKey, h3, h2.Sum(nil)) if err != nil { - return nil, err + return err } sig64 := base64.StdEncoding.EncodeToString(sig) @@ -210,32 +210,17 @@ func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { } } 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 + *email = append([]byte(dHeader), *email...) + return nil } // canonicalize returns canonicalized version of header and body -func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body []byte, err error) { - var email []byte +func canonicalize(email *[]byte, options sigOptions) (headers, body []byte, err error) { body = []byte{} rxReduceWS := regexp.MustCompile(`[ \t]+`) - email, err = ioutil.ReadAll(emailReader) - emailReader.Seek(0, 0) - if err != nil { - return - } - // TODO: \n -> \r\n - parts := bytes.SplitN(email, []byte{13, 10, 13, 10}, 2) + parts := bytes.SplitN(*email, []byte{13, 10, 13, 10}, 2) if len(parts) != 2 { return headers, body, ErrBadMailFormat diff --git a/dkim_test.go b/dkim_test.go index 70e277c..5384aff 100644 --- a/dkim_test.go +++ b/dkim_test.go @@ -1,8 +1,7 @@ package dkim import ( - "bytes" - "io/ioutil" + //"fmt" "testing" "github.com/stretchr/testify/assert" @@ -35,7 +34,7 @@ kS5vLkzRI84eiJrm3+IieUqIIicsO+WYxQs+JgVx5XhpPjX4SQjHtwEC2xKkWnEv selector = "test" ) -var email = "Received: (qmail 28277 invoked from network); 1 May 2015 09:43:37 -0000" + CRLF + +var emailBase = "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 + @@ -86,122 +85,121 @@ var signedRelaxedRelaxed = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=rela " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=byhiFWd0lAM1sqD1tl8S1DZtKNqgiEZp8jrGds6RRydnZkdX9rCPeL0Q5MYWBQ/JmQrml5" + CRLF + " pIghLwl/EshDBmNy65O6qO8pSSGgZmM3T7SRLMloex8bnrBJ4KSYcHV46639gVEWcBOKW0" + CRLF + - " h1djZu2jaTuxGeJzlFVtw3Arf2B93cc=" + CRLF + email + " h1djZu2jaTuxGeJzlFVtw3Arf2B93cc=" + CRLF + emailBase var signedSimpleSimple = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=simple/simple;" + CRLF + " s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF + " AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF + - " DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + email + " DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBase func Test_NewSigOptions(t *testing.T) { options := NewSigOptions() assert.Equal(t, "rsa-sha256", options.Algo) - assert.Equal(t, "simple/simple", options.Canonicalization) + assert.Equal(t, "relaxed/simple", options.Canonicalization) } -func Test_SignConfig(t *testing.T) { - emailReader := bytes.NewReader([]byte(email)) +/*func Test_SignConfig(t *testing.T) { + email := []byte(emailBase) + emailToTest := append([]byte(nil), email...) options := NewSigOptions() - _, err := Sign(emailReader, options) + err := Sign(&emailToTest, options) assert.NotNil(t, err) // && err No private key assert.EqualError(t, err, ErrSignPrivateKeyRequired.Error()) options.PrivateKey = privKey - _, err = Sign(emailReader, options) - emailReader.Seek(0, 0) + emailToTest = append([]byte(nil), email...) + err = Sign(&emailToTest, options) // Domain assert.EqualError(t, err, ErrSignDomainRequired.Error()) options.Domain = "toorop.fr" - _, err = Sign(emailReader, options) - emailReader.Seek(0, 0) + emailToTest = append([]byte(nil), email...) + err = Sign(&emailToTest, options) // Selector assert.Error(t, err, ErrSignSelectorRequired.Error()) options.Selector = "default" - _, err = Sign(emailReader, options) + emailToTest = append([]byte(nil), email...) + err = Sign(&emailToTest, options) assert.NoError(t, err) - emailReader.Seek(0, 0) // Canonicalization options.Canonicalization = "simple/relaxed/simple" - _, err = Sign(emailReader, options) + emailToTest = append([]byte(nil), email...) + err = Sign(&emailToTest, options) assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) - emailReader.Seek(0, 0) options.Canonicalization = "simple/relax" - _, err = Sign(emailReader, options) - emailReader.Seek(0, 0) + emailToTest = append([]byte(nil), email...) + err = Sign(&emailToTest, options) assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) - emailReader.Seek(0, 0) options.Canonicalization = "relaxed" - _, err = Sign(emailReader, options) + emailToTest = append([]byte(nil), email...) + err = Sign(&emailToTest, options) assert.NoError(t, err) - emailReader.Seek(0, 0) options.Canonicalization = "SiMple/relAxed" - _, err = Sign(emailReader, options) + emailToTest = append([]byte(nil), email...) + err = Sign(&emailToTest, options) assert.NoError(t, err) - emailReader.Seek(0, 0) // header options.Headers = []string{"toto"} - _, err = Sign(emailReader, options) + emailToTest = append([]byte(nil), email...) + err = Sign(&emailToTest, options) assert.EqualError(t, err, ErrSignHeaderShouldContainsFrom.Error()) - emailReader.Seek(0, 0) options.Headers = []string{"To", "From"} - _, err = Sign(emailReader, options) + emailToTest = append([]byte(nil), email...) + err = Sign(&emailToTest, options) assert.NoError(t, err) - emailReader.Seek(0, 0) + } func Test_canonicalize(t *testing.T) { - emailReader := bytes.NewReader([]byte(email)) + email := []byte(emailBase) + emailToTest := append([]byte(nil), 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) + header, body, err := canonicalize(&emailToTest, options) assert.NoError(t, err) assert.Equal(t, []byte(headerSimple), header) assert.Equal(t, []byte(bodySimple), body) // relaxed/relaxed + emailToTest = append([]byte(nil), email...) options.Canonicalization = "relaxed/relaxed" - header, body, err = canonicalize(emailReader, options) + header, body, err = canonicalize(&emailToTest, 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)) + email := []byte(emailBase) + emailRelaxed := append([]byte(nil), email...) options := NewSigOptions() options.PrivateKey = privKey options.Domain = domain options.Selector = selector - //options.AddSignatureTimestamp = true //options.SignatureExpireIn = 3600 options.BodyLength = 5 options.Headers = []string{"from", "date", "mime-version", "received", "received"} + options.AddSignatureTimestamp = false options.Canonicalization = "relaxed/relaxed" - signedEmailReader, err := Sign(emailReader, options) + err := Sign(&emailRelaxed, options) assert.NoError(t, err) - raw, _ := ioutil.ReadAll(signedEmailReader) - emailReader.Seek(0, 0) - assert.Equal(t, []byte(signedRelaxedRelaxed), raw) + assert.Equal(t, []byte(signedRelaxedRelaxed), emailRelaxed) options.Canonicalization = "simple/simple" - emailReader, err = Sign(emailReader, options) - assert.NoError(t, err) - raw, _ = ioutil.ReadAll(emailReader) - assert.Equal(t, []byte(signedSimpleSimple), raw) + emailSimple := append([]byte(nil), email...) + err = Sign(&emailSimple, options) + assert.Equal(t, []byte(signedSimpleSimple), emailSimple) - //raw, _ = ioutil.ReadAll(emailReader) - //fmt.Println(string(raw)) } diff --git a/watch b/watch index 13bcff7..82b5844 100755 --- a/watch +++ b/watch @@ -1,4 +1,4 @@ while true do -inotifywait -q -r -e modify,attrib,close_write,move,create,delete . && go test -v +inotifywait -q -r -e modify,attrib,close_write,move,create,delete . && echo "--------------" && go test -v done \ No newline at end of file