poc verify
This commit is contained in:
155
dkim.go
155
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
51
pubKeyRep.go
51
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user