poc verify

This commit is contained in:
Stéphane Depierrepont aka Toorop
2015-05-13 20:51:10 +02:00
parent 944e6d3610
commit dc14f06d84
5 changed files with 211 additions and 40 deletions

155
dkim.go
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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")
)

View File

@@ -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
}
}
}
}
// if no pubkey
if pkr.PubKey == (rsa.PublicKey{}) {
return nil, PERMFAIL, ErrVerifyNoKey
}
// TODO: If no pubkey
fmt.Println(txt, err)
return nil, SUCCESS, nil
return pkr, SUCCESS, nil
}