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 }