From 7206fc7daf2a2c3c364dc968e868761aa06d772c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Depierrepont=20aka=20Toorop?= Date: Tue, 12 May 2015 11:59:34 +0200 Subject: [PATCH] add struct for pubkey representation --- dkim.go | 82 +++++++++++++++++++++++++++++++++++++-------------- dkimHeader.go | 60 ++++++++++++++++++++++++++++++------- dkim_test.go | 40 +++++++++++++++---------- errors.go | 19 ++++++++++-- pubKeyRep.go | 26 ++++++++++++++++ 5 files changed, 176 insertions(+), 51 deletions(-) create mode 100644 pubKeyRep.go diff --git a/dkim.go b/dkim.go index 95f43b1..a1724d7 100644 --- a/dkim.go +++ b/dkim.go @@ -14,6 +14,7 @@ import ( //"fmt" "hash" //"io/ioutil" + //"net" "regexp" "strings" ) @@ -25,6 +26,15 @@ const ( MaxHeaderLineLength = 70 ) +type VerifyOutput int + +const ( + SUCCESS VerifyOutput = 1 + iota + PERMFAIL + TEMPFAIL + NOTSIGNED +) + // sigOptions represents signing options type sigOptions struct { @@ -143,31 +153,24 @@ func Sign(email *[]byte, options sigOptions) error { } // hash body - var bodyHash string - var h1, h2 hash.Hash + var h2 hash.Hash var h3 crypto.Hash signHash := strings.Split(options.Algo, "-") if signHash[1] == "sha1" { - h1 = sha1.New() + //h1 = sha1.New() h2 = sha1.New() h3 = crypto.SHA1 } else { - h1 = sha256.New() + //h1 = sha256.New() h2 = sha256.New() h3 = crypto.SHA256 } - // if l tag (body length) - if options.BodyLength != 0 { - if uint(len(body)) < options.BodyLength { - return ErrBadDKimTagLBodyTooShort - } - body = body[0:options.BodyLength] + bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength) + if err != nil { + return err } - h1.Write(body) - bodyHash = base64.StdEncoding.EncodeToString(h1.Sum(nil)) - // Get dkim header base dkimHeader := NewDkimHeaderBySigOptions(options) dHeader := dkimHeader.GetHeaderBaseForSigning(bodyHash) @@ -209,27 +212,41 @@ func Sign(email *[]byte, options sigOptions) error { // state: SUCCESS or PERMFAIL or TEMPFAIL or NOTSIGNED // msg: a complementary message (if needed) // error: if an error occurs during verification -func Verify(email *[]byte) (state, msg string, err error) { +func Verify(email *[]byte) (VerifyOutput, error) { // parse email dkimHeader, err := NewFromEmail(email) if err != nil { if err == ErrDkimHeaderNotFound { - return "NOTSIGNED", ErrDkimHeaderNotFound.Error(), nil + return NOTSIGNED, ErrDkimHeaderNotFound } else { - return "PERMFAIL", err.Error(), err + return PERMFAIL, err } } - println(dkimHeader) - // // Normalize - headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) + // Normalize + _, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) if err != nil { - return "PERMFAIL", err.Error(), err + return PERMFAIL, err + } + sigHash := strings.Split(dkimHeader.Algorithm, "-") + + // expired ? TODO + + // get body hash + bodyHash, err := getBodyHash(&body, sigHash[0], dkimHeader.BodyLength) + if err != nil { + return PERMFAIL, err + } + println(bodyHash) + if bodyHash != dkimHeader.BodyHash { + return PERMFAIL, ErrVerifyBodyHash } - // HERE + // we do not set quesry method because if it's other validation failed earlier + pubKey, err := newPubKeyFromDnsTxt(dkimHeader.Selector, dkimHeader.Domain) + println(pubKey) - return "SUCCESS", "", nil + return SUCCESS, nil } // canonicalize returns canonicalized version of header and body @@ -386,6 +403,27 @@ func canonicalizeHeader(header string, algo string) (string, error) { return header, ErrSignBadCanonicalization } +// getBodyHash return the hash (bas64encoded) of the body +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 + // if l tag (body length) + if bodyLength != 0 { + if uint(len(toH)) < bodyLength { + return "", ErrBadDKimTagLBodyTooShort + } + toH = toH[0:bodyLength] + } + + h.Write(toH) + return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil +} + // removeFWS removes all FWS from string func removeFWS(in string) string { rxReduceWS := regexp.MustCompile(`[ \t]+`) diff --git a/dkimHeader.go b/dkimHeader.go index 9cba0bd..11f1525 100644 --- a/dkimHeader.go +++ b/dkimHeader.go @@ -2,7 +2,7 @@ package dkim import ( "bytes" - "errors" + //"errors" "fmt" "net/mail" "net/textproto" @@ -271,6 +271,7 @@ func NewFromEmail(email *[]byte) (*DkimHeader, error) { return keep, nil } +// parseDkHeader parse raw dkim header func parseDkHeader(header string) (dkh *DkimHeader, err error) { dkh = new(DkimHeader) @@ -301,7 +302,7 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) { switch flag { case "v": if data != "1" { - return nil, ErrDkimVersionUnsuported + return nil, ErrDkimVersionNotsupported } dkh.Version = data mandatoryFlags["v"] = true @@ -313,27 +314,51 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) { mandatoryFlags["a"] = true case "b": dkh.SignatureData = removeFWS(data) - mandatoryFlags["b"] = true + if len(dkh.SignatureData) != 0 { + mandatoryFlags["b"] = true + } case "bh": dkh.BodyHash = removeFWS(data) - mandatoryFlags["bh"] = true + if len(dkh.BodyHash) != 0 { + mandatoryFlags["bh"] = true + } case "d": dkh.Domain = strings.ToLower(data) - mandatoryFlags["d"] = true + if len(dkh.Domain) != 0 { + mandatoryFlags["d"] = true + } case "h": data = strings.ToLower(data) dkh.Headers = strings.Split(data, ":") - mandatoryFlags["h"] = true + if len(dkh.Headers) != 0 { + mandatoryFlags["h"] = true + } + fromFound := false + for _, h := range dkh.Headers { + if h == "from" { + fromFound = true + } + } + if !fromFound { + return nil, ErrDkimHeaderNoFromInHTag + } case "s": dkh.Selector = strings.ToLower(data) - mandatoryFlags["s"] = true + if len(dkh.Selector) != 0 { + mandatoryFlags["s"] = true + } case "c": dkh.MessageCanonicalization, err = validateCanonicalization(strings.ToLower(data)) if err != nil { - return + return nil, err } case "i": - dkh.Auid = data + if data != "" { + if !strings.HasSuffix(data, dkh.Domain) { + return nil, ErrDkimHeaderDomainMismatch + } + dkh.Auid = data + } case "l": ui, err := strconv.ParseUint(data, 10, 32) if err != nil { @@ -342,6 +367,9 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) { dkh.BodyLength = uint(ui) case "q": dkh.QueryMethods = strings.Split(data, ":") + if len(dkh.QueryMethods) == 0 || strings.ToLower(dkh.QueryMethods[0]) != "dns/txt" { + return nil, errQueryMethodNotsupported + } case "t": ts, err := strconv.ParseInt(data, 10, 64) if err != nil { @@ -361,12 +389,22 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) { } // All mandatory flags are in ? - for f, p := range mandatoryFlags { + for _, p := range mandatoryFlags { if !p { - return nil, errors.New("missing '" + f + "' flag in DKIM header") + return nil, ErrDkimHeaderMissingRequiredTag } } + // default for i/Auid + if dkh.Auid == "" { + dkh.Auid = "@" + dkh.Domain + } + + // defaut for query method + if len(dkh.QueryMethods) == 0 { + dkh.QueryMethods = []string{"dns/text"} + } + return dkh, nil } diff --git a/dkim_test.go b/dkim_test.go index 74296e3..944c0ae 100644 --- a/dkim_test.go +++ b/dkim_test.go @@ -112,7 +112,7 @@ var signedSimpleSimple = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=simple " DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBase var signedNoFrom = "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 + + " s=test; d=tmail.io; h=from:date:mime-version:received:received;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF + " AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF + @@ -258,28 +258,38 @@ func Test_Sign(t *testing.T) { func Test_Verify(t *testing.T) { // no DKIM header email := []byte(emailBase) - status, msg, err := Verify(&email) - assert.NoError(t, err) - assert.Equal(t, "NOTSIGNED", status) - assert.Equal(t, ErrDkimHeaderNotFound.Error(), msg) + status, err := Verify(&email) + assert.Equal(t, NOTSIGNED, status) + assert.Equal(t, ErrDkimHeaderNotFound, err) // No From email = []byte(signedNoFrom) - status, msg, err = Verify(&email) - assert.NoError(t, err) - assert.Equal(t, "SUCCESS", status) + status, err = Verify(&email) + assert.Equal(t, ErrVerifyBodyHash, err) + assert.Equal(t, PERMFAIL, status) // cause we use dkheader of the "with from" email // missing mandatory 'a' flag email = []byte(signedMissingFlag) - status, msg, err = Verify(&email) + status, err = Verify(&email) assert.Error(t, err) - assert.Equal(t, "PERMFAIL", status) - assert.Equal(t, "missing 'a' flag in DKIM header", msg) + assert.Equal(t, PERMFAIL, status) + assert.Equal(t, ErrDkimHeaderMissingRequiredTag, err) // missing bad algo email = []byte(signedBadAlgo) - status, msg, err = Verify(&email) - assert.Error(t, err) - assert.Equal(t, "PERMFAIL", status) - assert.Equal(t, ErrSignBadAlgo.Error(), msg) + status, err = Verify(&email) + assert.Equal(t, PERMFAIL, status) + assert.Equal(t, ErrSignBadAlgo, err) + + // relaxed + email = []byte(signedRelaxedRelaxed) + status, err = Verify(&email) + assert.NoError(t, err) + assert.Equal(t, SUCCESS, status) + + // simple + email = []byte(signedSimpleSimple) + status, err = Verify(&email) + assert.NoError(t, err) + assert.Equal(t, SUCCESS, status) } diff --git a/errors.go b/errors.go index 2195b26..466296a 100644 --- a/errors.go +++ b/errors.go @@ -15,7 +15,7 @@ var ( ErrSignSelectorRequired = errors.New("Selector is required") // If Headers is specified it should at least contain 'from' - ErrSignHeaderShouldContainsFrom = errors.New("Header must contains 'from' field") + ErrSignHeaderShouldContainsFrom = errors.New("header must contains 'from' field") // If bad Canonicalization parameter ErrSignBadCanonicalization = errors.New("bad Canonicalization parameter") @@ -41,8 +41,21 @@ var ( // ErrDkimHeaderBTagNotFound when there's no b tag ErrDkimHeaderBTagNotFound = errors.New("no tag 'b' found in dkim header") - ErrDkimHeaderMissingTagV = errors.New("no tag 'v' found in dkim header") + // ErrDkimHeaderNoFromInHTag + ErrDkimHeaderNoFromInHTag = errors.New("'from' header is missing in h tag") + + // ErrDkimHeaderMissingRequiredTag when a required tag is missing + ErrDkimHeaderMissingRequiredTag = errors.New("signature missing required tag") + + // ErrDkimHeaderDomainMismatch if i tag is not a sub domain of d tag + ErrDkimHeaderDomainMismatch = errors.New("domain mismatch") // Version not supported - ErrDkimVersionUnsuported = errors.New("unsuported DKIM version") + ErrDkimVersionNotsupported = errors.New("incompatible version") + + // Query method unsopported + errQueryMethodNotsupported = errors.New("query method not supported") + + // ErrVerifyBodyHash when body hash doesn't verify + ErrVerifyBodyHash = errors.New("body hash did not verify") ) diff --git a/pubKeyRep.go b/pubKeyRep.go new file mode 100644 index 0000000..84ebb4e --- /dev/null +++ b/pubKeyRep.go @@ -0,0 +1,26 @@ +package dkim + +import ( + "crypto/rsa" + "fmt" + "net" +) + +// pubKeyRep represents a parsed version of public key record +type pubKeyRep struct { + Version string + HashAlgo []string + KeyType string + Note string + PubKey rsa.PublicKey + ServiceType []string + FlagTesting bool // flag y + FlagIMustBeD bool // flag i +} + +func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, error) { + txt, err := net.LookupTXT(selector + "._domainkey." + domain) + fmt.Println(txt, err) + + return nil, nil +}