add struct for pubkey representation
This commit is contained in:
82
dkim.go
82
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,30 +153,23 @@ 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
|
||||
bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = body[0:options.BodyLength]
|
||||
}
|
||||
|
||||
h1.Write(body)
|
||||
bodyHash = base64.StdEncoding.EncodeToString(h1.Sum(nil))
|
||||
|
||||
// Get dkim header base
|
||||
dkimHeader := NewDkimHeaderBySigOptions(options)
|
||||
@@ -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]+`)
|
||||
|
||||
@@ -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)
|
||||
if len(dkh.SignatureData) != 0 {
|
||||
mandatoryFlags["b"] = true
|
||||
}
|
||||
case "bh":
|
||||
dkh.BodyHash = removeFWS(data)
|
||||
if len(dkh.BodyHash) != 0 {
|
||||
mandatoryFlags["bh"] = true
|
||||
}
|
||||
case "d":
|
||||
dkh.Domain = strings.ToLower(data)
|
||||
if len(dkh.Domain) != 0 {
|
||||
mandatoryFlags["d"] = true
|
||||
}
|
||||
case "h":
|
||||
data = strings.ToLower(data)
|
||||
dkh.Headers = strings.Split(data, ":")
|
||||
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)
|
||||
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":
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
40
dkim_test.go
40
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)
|
||||
}
|
||||
|
||||
19
errors.go
19
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")
|
||||
)
|
||||
|
||||
26
pubKeyRep.go
Normal file
26
pubKeyRep.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user