add struct for pubkey representation
This commit is contained in:
82
dkim.go
82
dkim.go
@@ -14,6 +14,7 @@ import (
|
|||||||
//"fmt"
|
//"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
//"io/ioutil"
|
//"io/ioutil"
|
||||||
|
//"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,15 @@ const (
|
|||||||
MaxHeaderLineLength = 70
|
MaxHeaderLineLength = 70
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type VerifyOutput int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SUCCESS VerifyOutput = 1 + iota
|
||||||
|
PERMFAIL
|
||||||
|
TEMPFAIL
|
||||||
|
NOTSIGNED
|
||||||
|
)
|
||||||
|
|
||||||
// sigOptions represents signing options
|
// sigOptions represents signing options
|
||||||
type sigOptions struct {
|
type sigOptions struct {
|
||||||
|
|
||||||
@@ -143,31 +153,24 @@ func Sign(email *[]byte, options sigOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hash body
|
// hash body
|
||||||
var bodyHash string
|
var h2 hash.Hash
|
||||||
var h1, h2 hash.Hash
|
|
||||||
var h3 crypto.Hash
|
var h3 crypto.Hash
|
||||||
signHash := strings.Split(options.Algo, "-")
|
signHash := strings.Split(options.Algo, "-")
|
||||||
if signHash[1] == "sha1" {
|
if signHash[1] == "sha1" {
|
||||||
h1 = sha1.New()
|
//h1 = sha1.New()
|
||||||
h2 = sha1.New()
|
h2 = sha1.New()
|
||||||
h3 = crypto.SHA1
|
h3 = crypto.SHA1
|
||||||
} else {
|
} else {
|
||||||
h1 = sha256.New()
|
//h1 = sha256.New()
|
||||||
h2 = sha256.New()
|
h2 = sha256.New()
|
||||||
h3 = crypto.SHA256
|
h3 = crypto.SHA256
|
||||||
}
|
}
|
||||||
|
|
||||||
// if l tag (body length)
|
bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength)
|
||||||
if options.BodyLength != 0 {
|
if err != nil {
|
||||||
if uint(len(body)) < options.BodyLength {
|
return err
|
||||||
return ErrBadDKimTagLBodyTooShort
|
|
||||||
}
|
|
||||||
body = body[0:options.BodyLength]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1.Write(body)
|
|
||||||
bodyHash = base64.StdEncoding.EncodeToString(h1.Sum(nil))
|
|
||||||
|
|
||||||
// Get dkim header base
|
// Get dkim header base
|
||||||
dkimHeader := NewDkimHeaderBySigOptions(options)
|
dkimHeader := NewDkimHeaderBySigOptions(options)
|
||||||
dHeader := dkimHeader.GetHeaderBaseForSigning(bodyHash)
|
dHeader := dkimHeader.GetHeaderBaseForSigning(bodyHash)
|
||||||
@@ -209,27 +212,41 @@ func Sign(email *[]byte, options sigOptions) error {
|
|||||||
// state: SUCCESS or PERMFAIL or TEMPFAIL or NOTSIGNED
|
// state: SUCCESS or PERMFAIL or TEMPFAIL or NOTSIGNED
|
||||||
// msg: a complementary message (if needed)
|
// msg: a complementary message (if needed)
|
||||||
// error: if an error occurs during verification
|
// error: if an error occurs during verification
|
||||||
func Verify(email *[]byte) (state, msg string, err error) {
|
func Verify(email *[]byte) (VerifyOutput, error) {
|
||||||
// parse email
|
// parse email
|
||||||
dkimHeader, err := NewFromEmail(email)
|
dkimHeader, err := NewFromEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == ErrDkimHeaderNotFound {
|
if err == ErrDkimHeaderNotFound {
|
||||||
return "NOTSIGNED", ErrDkimHeaderNotFound.Error(), nil
|
return NOTSIGNED, ErrDkimHeaderNotFound
|
||||||
} else {
|
} else {
|
||||||
return "PERMFAIL", err.Error(), err
|
return PERMFAIL, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println(dkimHeader)
|
|
||||||
|
|
||||||
// // Normalize
|
// Normalize
|
||||||
headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers)
|
_, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers)
|
||||||
if err != nil {
|
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
|
// canonicalize returns canonicalized version of header and body
|
||||||
@@ -386,6 +403,27 @@ func canonicalizeHeader(header string, algo string) (string, error) {
|
|||||||
return header, ErrSignBadCanonicalization
|
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
|
// removeFWS removes all FWS from string
|
||||||
func removeFWS(in string) string {
|
func removeFWS(in string) string {
|
||||||
rxReduceWS := regexp.MustCompile(`[ \t]+`)
|
rxReduceWS := regexp.MustCompile(`[ \t]+`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package dkim
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
//"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
@@ -271,6 +271,7 @@ func NewFromEmail(email *[]byte) (*DkimHeader, error) {
|
|||||||
return keep, nil
|
return keep, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseDkHeader parse raw dkim header
|
||||||
func parseDkHeader(header string) (dkh *DkimHeader, err error) {
|
func parseDkHeader(header string) (dkh *DkimHeader, err error) {
|
||||||
dkh = new(DkimHeader)
|
dkh = new(DkimHeader)
|
||||||
|
|
||||||
@@ -301,7 +302,7 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) {
|
|||||||
switch flag {
|
switch flag {
|
||||||
case "v":
|
case "v":
|
||||||
if data != "1" {
|
if data != "1" {
|
||||||
return nil, ErrDkimVersionUnsuported
|
return nil, ErrDkimVersionNotsupported
|
||||||
}
|
}
|
||||||
dkh.Version = data
|
dkh.Version = data
|
||||||
mandatoryFlags["v"] = true
|
mandatoryFlags["v"] = true
|
||||||
@@ -313,27 +314,51 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) {
|
|||||||
mandatoryFlags["a"] = true
|
mandatoryFlags["a"] = true
|
||||||
case "b":
|
case "b":
|
||||||
dkh.SignatureData = removeFWS(data)
|
dkh.SignatureData = removeFWS(data)
|
||||||
mandatoryFlags["b"] = true
|
if len(dkh.SignatureData) != 0 {
|
||||||
|
mandatoryFlags["b"] = true
|
||||||
|
}
|
||||||
case "bh":
|
case "bh":
|
||||||
dkh.BodyHash = removeFWS(data)
|
dkh.BodyHash = removeFWS(data)
|
||||||
mandatoryFlags["bh"] = true
|
if len(dkh.BodyHash) != 0 {
|
||||||
|
mandatoryFlags["bh"] = true
|
||||||
|
}
|
||||||
case "d":
|
case "d":
|
||||||
dkh.Domain = strings.ToLower(data)
|
dkh.Domain = strings.ToLower(data)
|
||||||
mandatoryFlags["d"] = true
|
if len(dkh.Domain) != 0 {
|
||||||
|
mandatoryFlags["d"] = true
|
||||||
|
}
|
||||||
case "h":
|
case "h":
|
||||||
data = strings.ToLower(data)
|
data = strings.ToLower(data)
|
||||||
dkh.Headers = strings.Split(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":
|
case "s":
|
||||||
dkh.Selector = strings.ToLower(data)
|
dkh.Selector = strings.ToLower(data)
|
||||||
mandatoryFlags["s"] = true
|
if len(dkh.Selector) != 0 {
|
||||||
|
mandatoryFlags["s"] = true
|
||||||
|
}
|
||||||
case "c":
|
case "c":
|
||||||
dkh.MessageCanonicalization, err = validateCanonicalization(strings.ToLower(data))
|
dkh.MessageCanonicalization, err = validateCanonicalization(strings.ToLower(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
case "i":
|
case "i":
|
||||||
dkh.Auid = data
|
if data != "" {
|
||||||
|
if !strings.HasSuffix(data, dkh.Domain) {
|
||||||
|
return nil, ErrDkimHeaderDomainMismatch
|
||||||
|
}
|
||||||
|
dkh.Auid = data
|
||||||
|
}
|
||||||
case "l":
|
case "l":
|
||||||
ui, err := strconv.ParseUint(data, 10, 32)
|
ui, err := strconv.ParseUint(data, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -342,6 +367,9 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) {
|
|||||||
dkh.BodyLength = uint(ui)
|
dkh.BodyLength = uint(ui)
|
||||||
case "q":
|
case "q":
|
||||||
dkh.QueryMethods = strings.Split(data, ":")
|
dkh.QueryMethods = strings.Split(data, ":")
|
||||||
|
if len(dkh.QueryMethods) == 0 || strings.ToLower(dkh.QueryMethods[0]) != "dns/txt" {
|
||||||
|
return nil, errQueryMethodNotsupported
|
||||||
|
}
|
||||||
case "t":
|
case "t":
|
||||||
ts, err := strconv.ParseInt(data, 10, 64)
|
ts, err := strconv.ParseInt(data, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -361,12 +389,22 @@ func parseDkHeader(header string) (dkh *DkimHeader, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All mandatory flags are in ?
|
// All mandatory flags are in ?
|
||||||
for f, p := range mandatoryFlags {
|
for _, p := range mandatoryFlags {
|
||||||
if !p {
|
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
|
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
|
" DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBase
|
||||||
|
|
||||||
var signedNoFrom = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=simple/simple;" + CRLF +
|
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 +
|
" bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF +
|
||||||
" b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF +
|
" b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF +
|
||||||
" AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF +
|
" AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF +
|
||||||
@@ -258,28 +258,38 @@ func Test_Sign(t *testing.T) {
|
|||||||
func Test_Verify(t *testing.T) {
|
func Test_Verify(t *testing.T) {
|
||||||
// no DKIM header
|
// no DKIM header
|
||||||
email := []byte(emailBase)
|
email := []byte(emailBase)
|
||||||
status, msg, err := Verify(&email)
|
status, err := Verify(&email)
|
||||||
assert.NoError(t, err)
|
assert.Equal(t, NOTSIGNED, status)
|
||||||
assert.Equal(t, "NOTSIGNED", status)
|
assert.Equal(t, ErrDkimHeaderNotFound, err)
|
||||||
assert.Equal(t, ErrDkimHeaderNotFound.Error(), msg)
|
|
||||||
|
|
||||||
// No From
|
// No From
|
||||||
email = []byte(signedNoFrom)
|
email = []byte(signedNoFrom)
|
||||||
status, msg, err = Verify(&email)
|
status, err = Verify(&email)
|
||||||
assert.NoError(t, err)
|
assert.Equal(t, ErrVerifyBodyHash, err)
|
||||||
assert.Equal(t, "SUCCESS", status)
|
assert.Equal(t, PERMFAIL, status) // cause we use dkheader of the "with from" email
|
||||||
|
|
||||||
// missing mandatory 'a' flag
|
// missing mandatory 'a' flag
|
||||||
email = []byte(signedMissingFlag)
|
email = []byte(signedMissingFlag)
|
||||||
status, msg, err = Verify(&email)
|
status, err = Verify(&email)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, "PERMFAIL", status)
|
assert.Equal(t, PERMFAIL, status)
|
||||||
assert.Equal(t, "missing 'a' flag in DKIM header", msg)
|
assert.Equal(t, ErrDkimHeaderMissingRequiredTag, err)
|
||||||
|
|
||||||
// missing bad algo
|
// missing bad algo
|
||||||
email = []byte(signedBadAlgo)
|
email = []byte(signedBadAlgo)
|
||||||
status, msg, err = Verify(&email)
|
status, err = Verify(&email)
|
||||||
assert.Error(t, err)
|
assert.Equal(t, PERMFAIL, status)
|
||||||
assert.Equal(t, "PERMFAIL", status)
|
assert.Equal(t, ErrSignBadAlgo, err)
|
||||||
assert.Equal(t, ErrSignBadAlgo.Error(), msg)
|
|
||||||
|
// 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")
|
ErrSignSelectorRequired = errors.New("Selector is required")
|
||||||
|
|
||||||
// If Headers is specified it should at least contain 'from'
|
// 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
|
// If bad Canonicalization parameter
|
||||||
ErrSignBadCanonicalization = errors.New("bad Canonicalization parameter")
|
ErrSignBadCanonicalization = errors.New("bad Canonicalization parameter")
|
||||||
@@ -41,8 +41,21 @@ var (
|
|||||||
// ErrDkimHeaderBTagNotFound when there's no b tag
|
// ErrDkimHeaderBTagNotFound when there's no b tag
|
||||||
ErrDkimHeaderBTagNotFound = errors.New("no tag 'b' found in dkim header")
|
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
|
// 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