diff --git a/dkim.go b/dkim.go index 55aa1a8..922fbf2 100644 --- a/dkim.go +++ b/dkim.go @@ -17,6 +17,7 @@ import ( //"net" "regexp" "strings" + "time" ) const ( @@ -224,31 +225,56 @@ func Verify(email *[]byte) (VerifyOutput, error) { } // Normalize - _, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) + headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) if err != nil { return PERMFAIL, err } sigHash := strings.Split(dkimHeader.Algorithm, "-") - // expired ? TODO + // expired ? + if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() { + return PERMFAIL, ErrVerifySignatureHasExpired + } // get body hash - bodyHash, err := getBodyHash(&body, sigHash[0], dkimHeader.BodyLength) + bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength) if err != nil { return PERMFAIL, err } - println(bodyHash) if bodyHash != dkimHeader.BodyHash { return PERMFAIL, ErrVerifyBodyHash } - // we do not set quesry method because if it's other validation failed earlier + // we do not set query method because if it's others, validation failed earlier pubKey, verifyOutputOnError, err := newPubKeyFromDnsTxt(dkimHeader.Selector, dkimHeader.Domain) if err != nil { return verifyOutputOnError, err } - println(pubKey) + // check if hash algo are compatible + compatible := false + for _, algo := range pubKey.HashAlgo { + if sigHash[1] == algo { + compatible = true + break + } + } + if !compatible { + return PERMFAIL, ErrVerifyInappropriateHashAlgo + } + + // compute sig + dkimHeaderCano, err := canonicalizeHeader(dkimHeader.RawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0]) + if err != nil { + return TEMPFAIL, 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 PERMFAIL, err + } return SUCCESS, nil } @@ -257,22 +283,13 @@ func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, body = []byte{} rxReduceWS := regexp.MustCompile(`[ \t]+`) - // TODO: \n -> \r\n - parts := bytes.SplitN(*email, []byte{13, 10, 13, 10}, 2) - - if len(parts) != 2 { - return headers, body, ErrBadMailFormat - } - - // Empty body - if len(parts[1]) == 0 { - parts[1] = []byte{13, 10} - } + rawHeaders, rawBody, err := getHeadersBody(email) canonicalizations := strings.Split(cano, "/") // canonicalyze header - headersList := list.New() + headersList, err := getHeadersList(&rawHeaders) + /*headersList := list.New() currentHeader := []byte{} for _, line := range bytes.SplitAfter(parts[0], []byte{10}) { if line[0] == 32 || line[0] == 9 { @@ -289,7 +306,14 @@ func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, } currentHeader = append(currentHeader, line...) } + }*/ + + // debug + /*fmt.Println("-------------------------------------------") + for e := headersList.Front(); e != nil; e = e.Next() { + fmt.Printf("|%s|\n", e.Value.(string)) } + fmt.Println("-------------------------------------------")*/ // pour chaque header a conserver on traverse tous les headers dispo // If multi instance of a field we must keep it from the bottom to the top @@ -300,6 +324,7 @@ 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 @@ -333,7 +358,7 @@ func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, // of the body to a single "CRLF". // Note that a completely empty or missing body is canonicalized as a // single "CRLF"; that is, the canonicalized length will be 2 octets. - body = bytes.TrimRight(parts[1], "\r\n") + body = bytes.TrimRight(rawBody, "\r\n") body = append(body, []byte{13, 10}...) } else { // relaxed @@ -346,8 +371,8 @@ func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, // does not end with a CRLF, a CRLF is added. (For email, this is // only possible when using extensions to SMTP or non-SMTP transport // mechanisms.) - parts[1] = rxReduceWS.ReplaceAll(parts[1], []byte(" ")) - for _, line := range bytes.SplitAfter(parts[1], []byte{10}) { + rawBody = rxReduceWS.ReplaceAll(rawBody, []byte(" ")) + for _, line := range bytes.SplitAfter(rawBody, []byte{10}) { line = bytes.TrimRight(line, " \r\n") if len(line) != 0 { @@ -427,6 +452,57 @@ func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) { return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil } +/*func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, error) { + var h1 hash.Hash + var h2 crypto.Hash + switch algo { + case "sha1": + h1 = sha1.New() + h2 = crypto.SHA1 + break + case "sha256": + h1 = sha256.New() + h2 = crypto.SHA256 + break + default: + return "", ErrVerifyInappropriateHashAlgo + } + + // sign + h1.Write(*toSign) + sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil)) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(sig), nil + +}*/ + +func verifySignature(toSign []byte, sig64 string, key *rsa.PublicKey, algo string) error { + var h1 hash.Hash + var h2 crypto.Hash + switch algo { + case "sha1": + h1 = sha1.New() + h2 = crypto.SHA1 + break + case "sha256": + h1 = sha256.New() + h2 = crypto.SHA256 + break + default: + return ErrVerifyInappropriateHashAlgo + } + + //fmt.Printf("|%s|", toSign) + h1.Write(toSign) + sig, err := base64.StdEncoding.DecodeString(sig64) + if err != nil { + return err + } + return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig) +} + // removeFWS removes all FWS from string func removeFWS(in string) string { rxReduceWS := regexp.MustCompile(`[ \t]+`) @@ -452,3 +528,40 @@ func validateCanonicalization(cano string) (string, error) { } return cano, nil } + +// getHeadersList returns headers as list +func getHeadersList(rawHeader *[]byte) (*list.List, error) { + headersList := list.New() + currentHeader := []byte{} + for _, line := range bytes.SplitAfter(*rawHeader, []byte{10}) { + if line[0] == 32 || line[0] == 9 { + if len(currentHeader) == 0 { + return headersList, ErrBadMailFormatHeaders + } + currentHeader = append(currentHeader, line...) + } else { + // New header, save current if exists + if len(currentHeader) != 0 { + headersList.PushBack(string(currentHeader)) + currentHeader = []byte{} + } + currentHeader = append(currentHeader, line...) + } + } + headersList.PushBack(string(currentHeader)) + return headersList, nil +} + +// getHeadersBody return headers and body +func getHeadersBody(email *[]byte) ([]byte, []byte, error) { + // TODO: \n -> \r\n + parts := bytes.SplitN(*email, []byte{13, 10, 13, 10}, 2) + if len(parts) != 2 { + return []byte{}, []byte{}, ErrBadMailFormat + } + // Empty body + if len(parts[1]) == 0 { + parts[1] = []byte{13, 10} + } + return parts[0], parts[1], nil +} diff --git a/dkimHeader.go b/dkimHeader.go index 11f1525..bdf8cc8 100644 --- a/dkimHeader.go +++ b/dkimHeader.go @@ -192,7 +192,7 @@ type DkimHeader struct { // header with d = mail from domain) //HeaderMailFromDomain string - // RawForsign represents the raw part (with non canonicalization) of the header + // RawForsign represents the raw part (without canonicalization) of the header // used for computint sig in verify process RawForSign string } @@ -247,10 +247,30 @@ func NewFromEmail(email *[]byte) (*DkimHeader, error) { } } + // get raw dkim header + // we can't use m.header because header key will be converted with textproto.CanonicalMIMEHeaderKey + // ie if key in header is not DKIM-Signature but Dkim-Signature or DKIM-signature ot... other + // combination of case, verify will fail. + rawHeaders, _, err := getHeadersBody(email) + if err != nil { + return nil, ErrBadMailFormat + } + rawHeadersList, err := getHeadersList(&rawHeaders) + if err != nil { + return nil, err + } + dkHeaders := []string{} + for h := rawHeadersList.Front(); h != nil; h = h.Next() { + if strings.HasPrefix(strings.ToLower(h.Value.(string)), "dkim-signature") { + dkHeaders = append(dkHeaders, h.Value.(string)) + } + } + var keep *DkimHeader var keepErr error - for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] { - parsed, err := parseDkHeader(dk) + //for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] { + for _, h := range dkHeaders { + parsed, err := parseDkHeader(h) // if malformed dkim header try next if err != nil { keepErr = err @@ -275,6 +295,8 @@ func NewFromEmail(email *[]byte) (*DkimHeader, error) { func parseDkHeader(header string) (dkh *DkimHeader, err error) { dkh = new(DkimHeader) + keyVal := strings.SplitN(header, ":", 2) + t := strings.LastIndex(header, "b=") if t == -1 { return nil, ErrDkimHeaderBTagNotFound @@ -294,7 +316,7 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) { dkh.MessageCanonicalization = "simple/simple" dkh.QueryMethods = []string{"dns/txt"} - fs := strings.Split(header, ";") + fs := strings.Split(keyVal[1], ";") for _, f := range fs { flagData := strings.SplitN(f, "=", 2) flag := strings.ToLower(strings.TrimSpace(flagData[0])) @@ -313,7 +335,9 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) { } mandatoryFlags["a"] = true case "b": - dkh.SignatureData = removeFWS(data) + //dkh.SignatureData = removeFWS(data) + // remove all space + dkh.SignatureData = strings.Replace(removeFWS(data), " ", "", -1) if len(dkh.SignatureData) != 0 { mandatoryFlags["b"] = true } diff --git a/dkim_test.go b/dkim_test.go index 944c0ae..f184384 100644 --- a/dkim_test.go +++ b/dkim_test.go @@ -252,6 +252,8 @@ func Test_Sign(t *testing.T) { emailSimple := append([]byte(nil), email...) err = Sign(&emailSimple, options) assert.Equal(t, []byte(signedSimpleSimple), emailSimple) + //fmt.Println(signedSimpleSimple) + //fmt.Println(string(emailSimple)) } diff --git a/errors.go b/errors.go index 19eadf5..06f587e 100644 --- a/errors.go +++ b/errors.go @@ -79,4 +79,13 @@ var ( // ErrVerifyBadKey when we can't parse pubkey ErrVerifyBadKey = errors.New("unable to parse pub key") + + // ErrVerifyNoKey when no key is found on DNS record + ErrVerifyNoKey = errors.New("no public key found in DNS TXT") + + // ErrVerifySignatureHasExpired when signature has expired + ErrVerifySignatureHasExpired = errors.New("signature has expired") + + // ErrVerifyInappropriateHashAlgo when h tag in pub key doesn't contain hash algo from a tag of DKIM header + ErrVerifyInappropriateHashAlgo = errors.New("inappropriate has algorithm") ) diff --git a/pubKeyRep.go b/pubKeyRep.go index c8f1e78..2517f96 100644 --- a/pubKeyRep.go +++ b/pubKeyRep.go @@ -4,7 +4,6 @@ import ( "crypto/rsa" "crypto/x509" "encoding/base64" - "fmt" "net" "strings" ) @@ -40,6 +39,9 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, VerifyOutput, err pkr.Version = "DKIM1" pkr.HashAlgo = []string{"sha1", "sha256"} pkr.KeyType = "rsa" + pkr.ServiceType = []string{"all"} + pkr.FlagTesting = false + pkr.FlagIMustBeD = false // parsing, we keep the first record // TODO: if there is multiple record @@ -47,18 +49,22 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, VerifyOutput, err p := strings.Split(txt[0], ";") for i, data := range p { keyVal := strings.SplitN(data, "=", 2) + val := "" + if len(keyVal) > 1 { + val = strings.TrimSpace(keyVal[1]) + } switch strings.ToLower(strings.TrimSpace(keyVal[0])) { case "v": // RFC: is this tag is specified it MUST be the first in the record if i != 0 { return nil, PERMFAIL, ErrVerifyTagVMustBeTheFirst } - pkr.Version = strings.TrimSpace(keyVal[1]) + pkr.Version = val if pkr.Version != "DKIM1" { return nil, PERMFAIL, ErrVerifyVersionMusBeDkim1 } case "h": - p := strings.Split(strings.ToLower(keyVal[1]), ":") + p := strings.Split(strings.ToLower(val), ":") pkr.HashAlgo = []string{} for _, h := range p { h = strings.TrimSpace(h) @@ -71,34 +77,51 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, VerifyOutput, err pkr.HashAlgo = []string{"sha1", "sha256"} } case "k": - if strings.ToLower(strings.TrimSpace(keyVal[1])) != "rsa" { + if strings.ToLower(val) != "rsa" { return nil, PERMFAIL, ErrVerifyBadKeyType } case "n": - pkr.Note = strings.TrimSpace(keyVal[1]) + pkr.Note = val case "p": - rawkey := strings.TrimSpace(keyVal[1]) + rawkey := val if rawkey == "" { return nil, PERMFAIL, ErrVerifyRevokedKey } - // x509.ParsePKIXPublicKey(Dkim.PublicKey.PublicKey) un64, err := base64.StdEncoding.DecodeString(rawkey) if err != nil { return nil, PERMFAIL, ErrVerifyBadKey } pk, err := x509.ParsePKIXPublicKey(un64) pkr.PubKey = *pk.(*rsa.PublicKey) - // HERE case "s": + t := strings.Split(strings.ToLower(val), ":") + for _, tt := range t { + if tt == "*" { + pkr.ServiceType = []string{"all"} + break + } + if tt == "email" { + pkr.ServiceType = []string{"email"} + } + } case "t": - + flags := strings.Split(strings.ToLower(val), ":") + for _, flag := range flags { + if flag == "y" { + pkr.FlagTesting = true + continue + } + if flag == "s" { + pkr.FlagIMustBeD = true + } + } } - } - // TODO: If no pubkey + // if no pubkey + if pkr.PubKey == (rsa.PublicKey{}) { + return nil, PERMFAIL, ErrVerifyNoKey + } - fmt.Println(txt, err) - - return nil, SUCCESS, nil + return pkr, SUCCESS, nil }