From 00fde132a781e021eac74746b7dcaf5781bcecef Mon Sep 17 00:00:00 2001 From: Andres Erbsen Date: Tue, 18 Aug 2015 11:48:00 -0700 Subject: [PATCH 1/4] API cleanup --- dkim.go | 111 +++++++++++++++++--------------------------------- dkimHeader.go | 35 ++++++++-------- dkim_test.go | 75 ++++++++++++++++------------------ errors.go | 6 ++- pubKeyRep.go | 22 +++++----- 5 files changed, 105 insertions(+), 144 deletions(-) diff --git a/dkim.go b/dkim.go index 163da16..411b573 100644 --- a/dkim.go +++ b/dkim.go @@ -27,16 +27,6 @@ const ( type verifyOutput int -const ( - SUCCESS verifyOutput = 1 + iota - PERMFAIL - TEMPFAIL - NOTSIGNED - TESTINGSUCCESS - TESTINGPERMFAIL - TESTINGTEMPFAIL -) - // sigOptions represents signing options type SigOptions struct { @@ -98,40 +88,40 @@ func NewSigOptions() SigOptions { } // Sign signs an email -func Sign(email *[]byte, options SigOptions) error { +func Sign(email []byte, options SigOptions) ([]byte, error) { var privateKey *rsa.PrivateKey // PrivateKey if len(options.PrivateKey) == 0 { - return ErrSignPrivateKeyRequired + return nil, ErrSignPrivateKeyRequired } d, _ := pem.Decode(options.PrivateKey) key, err := x509.ParsePKCS1PrivateKey(d.Bytes) if err != nil { - return ErrCandNotParsePrivateKey + return nil, ErrCandNotParsePrivateKey } privateKey = key // Domain required if options.Domain == "" { - return ErrSignDomainRequired + return nil, ErrSignDomainRequired } // Selector required if options.Selector == "" { - return ErrSignSelectorRequired + return nil, ErrSignSelectorRequired } // Canonicalization options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization)) if err != nil { - return err + return nil, err } // Algo options.Algo = strings.ToLower(options.Algo) if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" { - return ErrSignBadAlgo + return nil, ErrSignBadAlgo } // Header must contain "from" @@ -144,21 +134,20 @@ func Sign(email *[]byte, options SigOptions) error { } } if !hasFrom { - return ErrSignHeaderShouldContainsFrom + return nil, ErrSignHeaderShouldContainsFrom } // Normalize headers, body, err := canonicalize(email, options.Canonicalization, options.Headers) if err != nil { - return err + return nil, err } signHash := strings.Split(options.Algo, "-") - // hash body - bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength) + bodyHash, err := getBodyHash(body, signHash[1], options.BodyLength) if err != nil { - return err + return nil, err } // Get dkim header base @@ -168,7 +157,7 @@ func Sign(email *[]byte, options SigOptions) error { canonicalizations := strings.Split(options.Canonicalization, "/") dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) if err != nil { - return err + return nil, err } headers = append(headers, []byte(dHeaderCanonicalized)...) headers = bytes.TrimRight(headers, " \r\n") @@ -189,35 +178,26 @@ func Sign(email *[]byte, options SigOptions) error { } } dHeader += subh + CRLF - *email = append([]byte(dHeader), *email...) - return nil + return append([]byte(dHeader), email...), 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) (verifyOutput, error) { +func Verify(email []byte) (dkimHeader *DKIMHeader, err error) { // parse email - dkimHeader, err := newDkimHeaderFromEmail(email) + dkimHeader, err = newDkimHeaderFromEmail(email) if err != nil { - if err == ErrDkimHeaderNotFound { - return NOTSIGNED, ErrDkimHeaderNotFound - } else { - return PERMFAIL, err - } + return } // we do not set query method because if it's others, validation failed earlier - pubKey, verifyOutputOnError, err := newPubKeyFromDnsTxt(dkimHeader.Selector, dkimHeader.Domain) + pubKey, err := newPubKeyFromDnsTxt(dkimHeader.Selector, dkimHeader.Domain) if err != nil { - return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting) + return nil, err } // Normalize headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) if err != nil { - return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) + return nil, err } sigHash := strings.Split(dkimHeader.Algorithm, "-") // check if hash algo are compatible @@ -229,60 +209,43 @@ func Verify(email *[]byte) (verifyOutput, error) { } } if !compatible { - return getVerifyOutput(PERMFAIL, ErrVerifyInappropriateHashAlgo, pubKey.FlagTesting) + return nil, ErrVerifyInappropriateHashAlgo } // expired ? if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() { - return getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting) - + return nil, ErrVerifySignatureHasExpired } - //println("|" + string(body) + "|") - // get body hash - bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength) + bodyHash, err := getBodyHash(body, sigHash[1], dkimHeader.BodyLength) if err != nil { - return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) + return nil, err } - //println(bodyHash) + if bodyHash != dkimHeader.BodyHash { - return getVerifyOutput(PERMFAIL, ErrVerifyBodyHash, pubKey.FlagTesting) + return nil, ErrVerifyBodyHash } // compute sig dkimHeaderCano, err := canonicalizeHeader(dkimHeader.RawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0]) if err != nil { - return getVerifyOutput(TEMPFAIL, err, pubKey.FlagTesting) + return nil, err } 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 nil, err } - 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 + if pubKey.FlagTesting { + err = ErrTesting } - 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 + return } // canonicalize returns canonicalized version of header and body -func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, err error) { +func canonicalize(email []byte, cano string, h []string) (headers, body []byte, err error) { body = []byte{} rxReduceWS := regexp.MustCompile(`[ \t]+`) @@ -302,7 +265,6 @@ func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, 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 @@ -408,14 +370,14 @@ func canonicalizeHeader(header string, algo string) (string, error) { } // getBodyHash return the hash (bas64encoded) of the body -func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) { +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 + toH := body // if l tag (body length) if bodyLength != 0 { if uint(len(toH)) < bodyLength { @@ -479,9 +441,10 @@ func verifySignature(toSign []byte, sig64 string, key *rsa.PublicKey, algo strin return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig) } +var rxReduceWS = regexp.MustCompile(`[ \t]+`) + // 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, " ") @@ -529,9 +492,9 @@ func getHeadersList(rawHeader *[]byte) (*list.List, error) { } // getHeadersBody return headers and body -func getHeadersBody(email *[]byte) ([]byte, []byte, error) { +func getHeadersBody(email []byte) ([]byte, []byte, error) { // 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 []byte{}, []byte{}, ErrBadMailFormat } diff --git a/dkimHeader.go b/dkimHeader.go index 11563a8..47e7666 100644 --- a/dkimHeader.go +++ b/dkimHeader.go @@ -10,7 +10,8 @@ import ( "time" ) -type dkimHeader struct { +// DKIMHeader +type DKIMHeader struct { // Version This tag defines the version of DKIM // specification that applies to the signature record. // tag v @@ -99,7 +100,7 @@ type dkimHeader struct { // Internationalized domain names MUST be encoded as A-labels, as // described in Section 2.3 of [RFC5890]. // tag i - Auid string + AUID string // Body length count (plain-text unsigned decimal integer; OPTIONAL, // default is entire body). This tag informs the Verifier of the @@ -197,14 +198,14 @@ type dkimHeader struct { } // NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value -func newDkimHeaderBySigOptions(options SigOptions) *dkimHeader { - h := new(dkimHeader) +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.AUID = options.Auid h.BodyLength = options.BodyLength h.QueryMethods = options.QueryMethods h.Selector = options.Selector @@ -221,8 +222,8 @@ func newDkimHeaderBySigOptions(options SigOptions) *dkimHeader { // 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)) +func newDkimHeaderFromEmail(email []byte) (*DKIMHeader, error) { + m, err := mail.ReadMessage(bytes.NewReader(email)) if err != nil { return nil, err } @@ -265,7 +266,7 @@ func newDkimHeaderFromEmail(email *[]byte) (*dkimHeader, error) { } } - var keep *dkimHeader + var keep *DKIMHeader var keepErr error //for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] { for _, h := range dkHeaders { @@ -291,8 +292,8 @@ func newDkimHeaderFromEmail(email *[]byte) (*dkimHeader, error) { } // parseDkHeader parse raw dkim header -func parseDkHeader(header string) (dkh *dkimHeader, err error) { - dkh = new(dkimHeader) +func parseDkHeader(header string) (dkh *DKIMHeader, err error) { + dkh = new(DKIMHeader) keyVal := strings.SplitN(header, ":", 2) @@ -384,7 +385,7 @@ func parseDkHeader(header string) (dkh *dkimHeader, err error) { if !strings.HasSuffix(data, dkh.Domain) { return nil, ErrDkimHeaderDomainMismatch } - dkh.Auid = data + dkh.AUID = data } case "l": ui, err := strconv.ParseUint(data, 10, 32) @@ -423,8 +424,8 @@ func parseDkHeader(header string) (dkh *dkimHeader, err error) { } // default for i/Auid - if dkh.Auid == "" { - dkh.Auid = "@" + dkh.Domain + if dkh.AUID == "" { + dkh.AUID = "@" + dkh.Domain } // defaut for query method @@ -438,7 +439,7 @@ func parseDkHeader(header string) (dkh *dkimHeader, err error) { // GetHeaderBase return base header for signers // Todo: some refactoring needed... -func (d *dkimHeader) getHeaderBaseForSigning(bodyHash string) string { +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 { @@ -448,12 +449,12 @@ func (d *dkimHeader) getHeaderBaseForSigning(bodyHash string) string { subh += " d=" + d.Domain + ";" // Auid - if len(d.Auid) != 0 { - if len(subh)+len(d.Auid)+4 > MaxHeaderLineLength { + if len(d.AUID) != 0 { + if len(subh)+len(d.AUID)+4 > MaxHeaderLineLength { h += subh + FWS subh = "" } - subh += " i=" + d.Auid + ";" + subh += " i=" + d.AUID + ";" } /*h := "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tmail.io; i=@tmail.io;" + FWS diff --git a/dkim_test.go b/dkim_test.go index d851789..be4436d 100644 --- a/dkim_test.go +++ b/dkim_test.go @@ -212,57 +212,57 @@ func Test_SignConfig(t *testing.T) { email := []byte(emailBase) emailToTest := append([]byte(nil), email...) options := NewSigOptions() - err := Sign(&emailToTest, options) + _, err := Sign(emailToTest, options) assert.NotNil(t, err) // && err No private key assert.EqualError(t, err, ErrSignPrivateKeyRequired.Error()) options.PrivateKey = []byte(privKey) emailToTest = append([]byte(nil), email...) - err = Sign(&emailToTest, options) + _, err = Sign(emailToTest, options) // Domain assert.EqualError(t, err, ErrSignDomainRequired.Error()) options.Domain = "toorop.fr" emailToTest = append([]byte(nil), email...) - err = Sign(&emailToTest, options) + _, err = Sign(emailToTest, options) // Selector assert.Error(t, err, ErrSignSelectorRequired.Error()) options.Selector = "default" emailToTest = append([]byte(nil), email...) - err = Sign(&emailToTest, options) + _, err = Sign(emailToTest, options) assert.NoError(t, err) // Canonicalization options.Canonicalization = "simple/relaxed/simple" emailToTest = append([]byte(nil), email...) - err = Sign(&emailToTest, options) + _, err = Sign(emailToTest, options) assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) options.Canonicalization = "simple/relax" emailToTest = append([]byte(nil), email...) - err = Sign(&emailToTest, options) + _, err = Sign(emailToTest, options) assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) options.Canonicalization = "relaxed" emailToTest = append([]byte(nil), email...) - err = Sign(&emailToTest, options) + _, err = Sign(emailToTest, options) assert.NoError(t, err) options.Canonicalization = "SiMple/relAxed" emailToTest = append([]byte(nil), email...) - err = Sign(&emailToTest, options) + _, err = Sign(emailToTest, options) assert.NoError(t, err) // header options.Headers = []string{"toto"} emailToTest = append([]byte(nil), email...) - err = Sign(&emailToTest, options) + _, err = Sign(emailToTest, options) assert.EqualError(t, err, ErrSignHeaderShouldContainsFrom.Error()) options.Headers = []string{"To", "From"} emailToTest = append([]byte(nil), email...) - err = Sign(&emailToTest, options) + _, err = Sign(emailToTest, options) assert.NoError(t, err) } @@ -274,18 +274,18 @@ func Test_canonicalize(t *testing.T) { options.Headers = []string{"from", "date", "mime-version", "received", "received", "In-Reply-To"} // simple/simple options.Canonicalization = "simple/simple" - header, body, err := canonicalize(&emailToTest, options.Canonicalization, options.Headers) + header, body, err := canonicalize(emailToTest, options.Canonicalization, options.Headers) assert.NoError(t, err) - assert.Equal(t, []byte(headerSimple), header) - assert.Equal(t, []byte(bodySimple), body) + assert.Equal(t, headerSimple, string(header)) + assert.Equal(t, bodySimple, string(body)) // relaxed/relaxed emailToTest = append([]byte(nil), email...) options.Canonicalization = "relaxed/relaxed" - header, body, err = canonicalize(&emailToTest, options.Canonicalization, options.Headers) + header, body, err = canonicalize(emailToTest, options.Canonicalization, options.Headers) assert.NoError(t, err) - assert.Equal(t, []byte(headerRelaxed), header) - assert.Equal(t, []byte(bodyRelaxed), body) + assert.Equal(t, headerRelaxed, string(header)) + assert.Equal(t, bodyRelaxed, string(body)) } @@ -301,77 +301,70 @@ func Test_Sign(t *testing.T) { options.AddSignatureTimestamp = false options.Canonicalization = "relaxed/relaxed" - err := Sign(&emailRelaxed, options) + emailRelaxed, err := Sign(emailRelaxed, options) assert.NoError(t, err) - assert.Equal(t, []byte(signedRelaxedRelaxed), emailRelaxed) + assert.Equal(t, signedRelaxedRelaxed, string(emailRelaxed)) options.BodyLength = 5 emailRelaxed = append([]byte(nil), email...) - err = Sign(&emailRelaxed, options) + emailRelaxed, err = Sign(emailRelaxed, options) assert.NoError(t, err) - assert.Equal(t, []byte(signedRelaxedRelaxedLength), emailRelaxed) + assert.Equal(t, signedRelaxedRelaxedLength, string(emailRelaxed)) options.BodyLength = 0 options.Canonicalization = "simple/simple" emailSimple := append([]byte(nil), email...) - err = Sign(&emailSimple, options) - assert.Equal(t, []byte(signedSimpleSimple), emailSimple) + emailSimple, err = Sign(emailSimple, options) + assert.Equal(t, signedSimpleSimple, string(emailSimple)) options.Headers = []string{"from", "subject", "date", "message-id"} memail := []byte(missingHeaderMail) - err = Sign(&memail, options) + _, err = Sign(memail, options) assert.NoError(t, err) options.BodyLength = 5 options.Canonicalization = "simple/simple" emailSimple = append([]byte(nil), email...) - err = Sign(&emailSimple, options) - assert.Equal(t, []byte(signedSimpleSimpleLength), emailSimple) + emailSimple, err = Sign(emailSimple, options) + assert.Equal(t, signedSimpleSimpleLength, string(emailSimple)) } func Test_Verify(t *testing.T) { // no DKIM header email := []byte(emailBase) - status, err := Verify(&email) - assert.Equal(t, NOTSIGNED, status) + _, err := Verify(email) assert.Equal(t, ErrDkimHeaderNotFound, err) // No From email = []byte(signedNoFrom) - status, err = Verify(&email) + _, err = Verify(email) assert.Equal(t, ErrVerifyBodyHash, err) - assert.Equal(t, TESTINGPERMFAIL, status) // cause we use dkheader of the "with from" email // missing mandatory 'a' flag email = []byte(signedMissingFlag) - status, err = Verify(&email) + _, err = Verify(email) assert.Error(t, err) - assert.Equal(t, PERMFAIL, status) assert.Equal(t, ErrDkimHeaderMissingRequiredTag, err) // missing bad algo email = []byte(signedBadAlgo) - status, err = Verify(&email) - assert.Equal(t, PERMFAIL, status) + _, err = Verify(email) assert.Equal(t, ErrSignBadAlgo, err) // relaxed email = []byte(signedRelaxedRelaxedLength) - status, err = Verify(&email) - assert.NoError(t, err) - assert.Equal(t, SUCCESS, status) + _, err = Verify(email) + assert.Equal(t, ErrTesting, err) // simple email = []byte(signedSimpleSimpleLength) - status, err = Verify(&email) - assert.NoError(t, err) - assert.Equal(t, SUCCESS, status) + _, err = Verify(email) + assert.Equal(t, ErrTesting, err) // gmail email = []byte(fromGmail) - status, err = Verify(&email) + _, err = Verify(email) assert.NoError(t, err) - assert.Equal(t, SUCCESS, status) } diff --git a/errors.go b/errors.go index 06f587e..136be13 100644 --- a/errors.go +++ b/errors.go @@ -62,7 +62,8 @@ var ( // ErrVerifyNoKeyForSignature ErrVerifyNoKeyForSignature = errors.New("no key for verify") - // ErrVerifyKeyUnavailable when service (dns) is anavailable + // ErrVerifyKeyUnavailable when service (dns) is anavailable. + // This error may be temporary in some cases. ErrVerifyKeyUnavailable = errors.New("key unavailable") // ErrVerifyTagVMustBeTheFirst if present the v tag must be the firts in the record @@ -88,4 +89,7 @@ var ( // ErrVerifyInappropriateHashAlgo when h tag in pub key doesn't contain hash algo from a tag of DKIM header ErrVerifyInappropriateHashAlgo = errors.New("inappropriate has algorithm") + + // ErrTesting + ErrTesting = errors.New("public key has testing flag set") ) diff --git a/pubKeyRep.go b/pubKeyRep.go index 84f3989..6ec248b 100644 --- a/pubKeyRep.go +++ b/pubKeyRep.go @@ -20,19 +20,19 @@ type pubKeyRep struct { FlagIMustBeD bool // flag i } -func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, error) { +func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, error) { txt, err := net.LookupTXT(selector + "._domainkey." + domain) if err != nil { if strings.HasSuffix(err.Error(), "no such host") { - return nil, PERMFAIL, ErrVerifyNoKeyForSignature + return nil, ErrVerifyNoKeyForSignature } else { - return nil, TEMPFAIL, ErrVerifyKeyUnavailable + return nil, ErrVerifyKeyUnavailable } } // empty record if len(txt) == 0 { - return nil, PERMFAIL, ErrVerifyNoKeyForSignature + return nil, ErrVerifyNoKeyForSignature } pkr := new(pubKeyRep) @@ -57,11 +57,11 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err case "v": // RFC: is this tag is specified it MUST be the first in the record if i != 0 { - return nil, PERMFAIL, ErrVerifyTagVMustBeTheFirst + return nil, ErrVerifyTagVMustBeTheFirst } pkr.Version = val if pkr.Version != "DKIM1" { - return nil, PERMFAIL, ErrVerifyVersionMusBeDkim1 + return nil, ErrVerifyVersionMusBeDkim1 } case "h": p := strings.Split(strings.ToLower(val), ":") @@ -78,18 +78,18 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err } case "k": if strings.ToLower(val) != "rsa" { - return nil, PERMFAIL, ErrVerifyBadKeyType + return nil, ErrVerifyBadKeyType } case "n": pkr.Note = val case "p": rawkey := val if rawkey == "" { - return nil, PERMFAIL, ErrVerifyRevokedKey + return nil, ErrVerifyRevokedKey } un64, err := base64.StdEncoding.DecodeString(rawkey) if err != nil { - return nil, PERMFAIL, ErrVerifyBadKey + return nil, ErrVerifyBadKey } pk, err := x509.ParsePKIXPublicKey(un64) pkr.PubKey = *pk.(*rsa.PublicKey) @@ -120,8 +120,8 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err // if no pubkey if pkr.PubKey == (rsa.PublicKey{}) { - return nil, PERMFAIL, ErrVerifyNoKey + return nil, ErrVerifyNoKey } - return pkr, SUCCESS, nil + return pkr, nil } From 47c4e9fcb7176b46b9370cc061da96230897d08c Mon Sep 17 00:00:00 2001 From: Andres Erbsen Date: Tue, 18 Aug 2015 12:09:42 -0700 Subject: [PATCH 2/4] allow mocking --- dkim.go | 32 +++++++++++++++++++++----------- dkim_test.go | 14 +++++++------- pubKeyRep.go | 12 +----------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/dkim.go b/dkim.go index 411b573..1296181 100644 --- a/dkim.go +++ b/dkim.go @@ -13,6 +13,7 @@ import ( "encoding/base64" "encoding/pem" "hash" + "net" "regexp" "strings" "time" @@ -25,8 +26,6 @@ const ( MaxHeaderLineLength = 70 ) -type verifyOutput int - // sigOptions represents signing options type SigOptions struct { @@ -181,26 +180,36 @@ func Sign(email []byte, options SigOptions) ([]byte, error) { return append([]byte(dHeader), email...), nil } -func Verify(email []byte) (dkimHeader *DKIMHeader, err error) { - // parse email +func Verify(email []byte, LookupTXT func(string) ([]string, error), now func() time.Time) (dkimHeader *DKIMHeader, err error) { + if LookupTXT == nil { + LookupTXT = net.LookupTXT + } + if now == nil { + now = time.Now + } + dkimHeader, err = newDkimHeaderFromEmail(email) if err != nil { return } - // we do not set query method because if it's others, validation failed earlier - pubKey, err := newPubKeyFromDnsTxt(dkimHeader.Selector, dkimHeader.Domain) + txt, err := LookupTXT(dkimHeader.Selector + "._domainkey." + dkimHeader.Domain) + if err != nil { + return nil, err + } + pubKey, err := newPubKeyFromDnsTxt(txt) if err != nil { return nil, err } - // Normalize headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) if err != nil { return nil, err } sigHash := strings.Split(dkimHeader.Algorithm, "-") - // check if hash algo are compatible + if len(sigHash) < 2 { + return nil, ErrVerifyInappropriateHashAlgo + } compatible := false for _, algo := range pubKey.HashAlgo { if sigHash[1] == algo { @@ -212,9 +221,10 @@ func Verify(email []byte) (dkimHeader *DKIMHeader, err error) { return nil, ErrVerifyInappropriateHashAlgo } - // expired ? - if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() { - return nil, ErrVerifySignatureHasExpired + if !dkimHeader.SignatureExpiration.IsZero() { + if dkimHeader.SignatureExpiration.Before(now()) { + return nil, ErrVerifySignatureHasExpired + } } bodyHash, err := getBodyHash(body, sigHash[1], dkimHeader.BodyLength) diff --git a/dkim_test.go b/dkim_test.go index be4436d..8bb9c39 100644 --- a/dkim_test.go +++ b/dkim_test.go @@ -333,38 +333,38 @@ func Test_Sign(t *testing.T) { func Test_Verify(t *testing.T) { // no DKIM header email := []byte(emailBase) - _, err := Verify(email) + _, err := Verify(email, nil, nil) assert.Equal(t, ErrDkimHeaderNotFound, err) // No From email = []byte(signedNoFrom) - _, err = Verify(email) + _, err = Verify(email, nil, nil) assert.Equal(t, ErrVerifyBodyHash, err) // missing mandatory 'a' flag email = []byte(signedMissingFlag) - _, err = Verify(email) + _, err = Verify(email, nil, nil) assert.Error(t, err) assert.Equal(t, ErrDkimHeaderMissingRequiredTag, err) // missing bad algo email = []byte(signedBadAlgo) - _, err = Verify(email) + _, err = Verify(email, nil, nil) assert.Equal(t, ErrSignBadAlgo, err) // relaxed email = []byte(signedRelaxedRelaxedLength) - _, err = Verify(email) + _, err = Verify(email, nil, nil) assert.Equal(t, ErrTesting, err) // simple email = []byte(signedSimpleSimpleLength) - _, err = Verify(email) + _, err = Verify(email, nil, nil) assert.Equal(t, ErrTesting, err) // gmail email = []byte(fromGmail) - _, err = Verify(email) + _, err = Verify(email, nil, nil) assert.NoError(t, err) } diff --git a/pubKeyRep.go b/pubKeyRep.go index 6ec248b..5c2eecd 100644 --- a/pubKeyRep.go +++ b/pubKeyRep.go @@ -4,7 +4,6 @@ import ( "crypto/rsa" "crypto/x509" "encoding/base64" - "net" "strings" ) @@ -20,16 +19,7 @@ type pubKeyRep struct { FlagIMustBeD bool // flag i } -func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, error) { - txt, err := net.LookupTXT(selector + "._domainkey." + domain) - if err != nil { - if strings.HasSuffix(err.Error(), "no such host") { - return nil, ErrVerifyNoKeyForSignature - } else { - return nil, ErrVerifyKeyUnavailable - } - } - +func newPubKeyFromDnsTxt(txt []string) (*pubKeyRep, error) { // empty record if len(txt) == 0 { return nil, ErrVerifyNoKeyForSignature From 7db05892c40ba340a14fa5303ad8f941a7f22301 Mon Sep 17 00:00:00 2001 From: Andres Erbsen Date: Tue, 18 Aug 2015 12:21:46 -0700 Subject: [PATCH 3/4] test for yahoo-inc.com --- dkim_test.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/dkim_test.go b/dkim_test.go index 8bb9c39..ea2b772 100644 --- a/dkim_test.go +++ b/dkim_test.go @@ -2,7 +2,9 @@ package dkim import ( //"fmt" + "strings" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -368,3 +370,84 @@ func Test_Verify(t *testing.T) { assert.NoError(t, err) } + +var yahooIncDKIMtest = strings.Replace(`X-Apparently-To: andreser@yahoo-inc.com; Mon, 17 Aug 2015 22:49:25 +0000 +Return-Path: +Received-SPF: pass (domain of yahoo-inc.com designates 216.145.54.109 as permitted sender) +X-YMailISG: mjZhcf4WLDsyM65yWRfgyfO_lZT.dRW6ZkL0mQ36QKSZ1wt8 + norPyPfS_RaocAsatZMUc76bWB9uuFubtxIu.6wHOaop_IvkFzIxMpIj0qV. + Lrx.L7iOLJ2Y5WVt6viLV7QS58O_2NzGwj3OIQL5EkGvSAZntHzX6fwew2_o + mtpmgrO9DKSOmSxs0mI1hgXdqr2U2oqrtF9ibc4Z2cFMaZ4R1JeYcprQW9Xu + X0YqkidSky.VEpst35uNTE.OMGZrIFHPzaKfF5GarnIJGSqhk.5NMjq_Bywg + 5LYpX9AoXaCFOQd0Tzp4raM0IUmhBRaGPPXUBbzqovVvuLdJ.clh6.kYtv_F + 5aNQtHP5cNqhPTooi1c_mZlh6phP12PMUVdx9WdfEmvVaN1Jumay.SzOtTPh + 89IA7pgAferCuLh5f_9lEkYLkFomW4SRwexAbpdfwm1R1CYprsZMQ1YhFZI3 + GinHyEiPUo48hxgTJgWIuv0oiCoDzd8exD5.u0ZW6Ztvy3UVvogbGCJ6KvXy + 7CT1iwdHcoCiGcoE9e7zEqZdH7GftkZGobaX83r3bzhhc0GVMmY29fB4BnZj + suHtpK.Cx7vY.hJvV_R_.QH5npxcM8ptVFLgkNW6tBzqF9GnbWtr7v2ERGjn + hewHjiEQAGbay6c19tw.3s0SEEhb0BdbxeGajeqNJhYLC8j18hRQR67oWyFF + LON7S1cfRM2sQKVWW4K0I7KMad7FrxEi6VJdfIVD8gLMW7uhlkowqOE9rhtj + 042FEnYc7kcrvL58Bj8v9TY3Z2Nl8HXifr6dGK_Kw9HK79We3O00cdZSWASu + R8pA_AB40d80d82.0crHu0oFFX6KFT8xkAipyIvPhK4bZz7r.NnBD1ZKq7ZF + TpBmxt0hbxWy_Qkz1M9BrzrGbbeSAFhAyyZqoPYsWy8FN5U3jzU.ZQygaK.E + DT18hIHBF2qN4R3JLVxA7zX1OfxL24UlPvuPaAERm9Wq4WRagcK7ysJt7.9b + WskH.vySl_.3mtF7yBFXOR_7_aIM54djcILP_MGhqEjJVbPp12KmbQ51cD_o + 76mHVraxIkOZV0eVal8V9QwIaAbbb9caFJcySJdUSIVvojxd6fneN83jCsD. + Df0Iz4J2pF0BYiVnY4.MIhUSZZtCjxBAK4roNSvdyVDEQdYPiJpQHoBIDCnj + mgQCRPWOCRXaaDMnFvBzJJ7_z04R5rB3vFg65xsBN0wyeDX1veLLsMHChAbp + 8RPEQFrsqmFFrXXRODtacXX1ZOZV1tTI +X-Originating-IP: [216.145.54.109] +Authentication-Results: mta2007.corp.mail.ne1.yahoo.com from=yahoo-inc.com; domainkeys=neutral (no sig); from=yahoo-inc.com; dkim=pass (ok) +Received: from 127.0.0.1 (EHLO mrout4.yahoo.com) (216.145.54.109) + by mta2007.corp.mail.ne1.yahoo.com with SMTPS; Mon, 17 Aug 2015 22:49:25 +0000 +Received: from omp1017.mail.ne1.yahoo.com (omp1017.mail.ne1.yahoo.com [98.138.89.161]) + by mrout4.yahoo.com (8.14.9/8.14.9/y.out) with ESMTP id t7HMn6pT004450 + (version=TLSv1/SSLv3 cipher=DHE-RSA-CAMELLIA256-SHA bits=256 verify=NO) + for ; Mon, 17 Aug 2015 15:49:06 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=yahoo-inc.com; + s=cobra; t=1439851746; + bh=Zg0pSZvCcMHE9S9qpkoEKeacBIM4T3Xu4TUSMEL4rXw=; + h=Date:From:Reply-To:To:Subject; + b=C+cq+oEeDf+21aR1gaYIeuqE9cwJBuT3leqtd1ktLtmd4R3HAWXkt8Wr18PeOicjT + +8IJeZ4t+D6UDq3cIHRblyK2+LRP514YDttLfNbQQ28BOlEaycS4ZbrRtwYR0/bJsJ + EekQ8FrwzHZQOlmrqeN4bVIAlI73X+OBynbLyDrw= +Received: (qmail 15334 invoked by uid 1000); 17 Aug 2015 22:49:06 -0000 +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yahoo-inc.com; s=ginc1024; t=1439851746; bh=Zg0pSZvCcMHE9S9qpkoEKeacBIM4T3Xu4TUSMEL4rXw=; h=Date:From:Reply-To:To:Message-ID:Subject:MIME-Version:Content-Type; b=Ut+tXUluIOFrGnFm6m0fvXuIQDIEulXFkWmj9bQSO0JN3gPiWfuh1bFhZBdnu2C4SREtTfrxHI8q5DGPjD8yg4LnxFh3HOuaf4Ttm8w72QGO1HxJCdwkNvu5W4mnFTEB8hdl2u5naE4JqjJtM291ZYIJGvxFA2J3+Snj/N2aG40= +X-YMail-OSG: G1B4VdwVM1lYA9kmxoxrGwEODiHeae6vbYVeBm754R2VWrC5KBM9pyd4ojSurOA + q0um_rXRvGr1aqpHntt5GL5mcITy4qZFZWIBKRlGdOvQKNsKMSzsglbrG0Io._.0dI8XBQ.DNWG3 + Z5uVt9prZqJLlJG.FcGrNnYQTiX.Q0HDTID4rDKM.sA6Z_CUAPOto0IFqnA9buS5R8Rjy3xqs5qf + krxUdQCFbVG.ML8Kl0WJfy8ZKxjg1mT7Nma.ZOA-- +Received: by 98.138.105.251; Mon, 17 Aug 2015 22:49:05 +0000 +Date: Mon, 17 Aug 2015 22:49:05 +0000 (UTC) +From: Andres Erbsen +Reply-To: Andres Erbsen +To: Andres Erbsen Erbsen +Message-ID: <408588803.6263873.1439851745104.JavaMail.yahoo@mail.yahoo.com> +Subject: end-to-end public key verification [test] +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_Part_6263872_19047179.1439851745102" +Content-Length: 622 + +------=_Part_6263872_19047179.1439851745102 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit + +fdsfasdfdasgawgasdgdsgadfgadsgdgadga +------=_Part_6263872_19047179.1439851745102 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: 7bit + +
fdsfasdfdasgawgasdgdsgadfgadsgdgadga
+------=_Part_6263872_19047179.1439851745102--`, "\n", "\r\n", -1) + +func TestYahooIncDKIM(t *testing.T) { + yIncTXT := func(string) ([]string, error) { + return []string{"v=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGDd1Fz/AblN4d1haW+4B/u8PXkpd/s/JFkCPqp0Zk8xZ/SEs15fsWmj7yZwfsgi04Bs1eJhUIGf0iufHvkK5ws5XKBfbw1hYBHexopqYT5JFERYJ3slNEG5EeB04kKWpECjoMkXhDWvUJrHaBqGAz2KQ1dKAzrtKqRN2IVcDbBQIDAQAB"}, nil + } + yIncTime := func() time.Time { return time.Unix(1439925628, 0) } + _, err := Verify([]byte(yahooIncDKIMtest), yIncTXT, yIncTime) + if err != nil { + t.Fatal(err) + } +} From 2cf1158195a2d155e1ff168b9efbd2a61dc7fb60 Mon Sep 17 00:00:00 2001 From: Andres Erbsen Date: Tue, 18 Aug 2015 12:24:19 -0700 Subject: [PATCH 4/4] readme --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4956739..6bf1033 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # go-dkim -DKIM package for Golang -[![GoDoc](https://godoc.org/github.com/toorop/go-dkim?status.svg)](https://godoc.org/github.com/toorop/go-dkim) +Fork of `toorop`'s DKIM with non-pointer API and bugfixes. + +[![GoDoc](https://godoc.org/github.com/andres-erbsen/go-dkim?status.svg)](https://godoc.org/github.com/andres-erbsen/go-dkim) ## Getting started ### Install ``` - go get github.com/toorop/go-dkim + go get github.com/andres-erbsen/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. @@ -16,7 +17,7 @@ see https://github.com/golang/go/issues/10482 fro more info. ```go import ( - dkim "github.com/toorop/go-dkim" + dkim "github.com/andres-erbsen/go-dkim" ) func main(){ @@ -33,8 +34,6 @@ func main(){ options.Canonicalization = "relaxed/relaxed" err := dkim.Sign(&email, options) // handle err.. - - // And... that's it, 'email' is signed ! Amazing© !!! } ``` @@ -46,11 +45,11 @@ import ( func main(){ // email is the email to verify (byte slice) - status, err := Verify(&email) - // handle status, err (see godoc for status) + _, err := Verify(email, nil, nil) } ``` ## Todo - [ ] handle z tag (copied header fields used for diagnostic use) +- [ ] handle multiple dns records