API cleanup

This commit is contained in:
Andres Erbsen
2015-08-18 11:48:00 -07:00
parent 4b92c72b9a
commit 00fde132a7
5 changed files with 105 additions and 144 deletions

111
dkim.go
View File

@@ -27,16 +27,6 @@ const (
type verifyOutput int type verifyOutput int
const (
SUCCESS verifyOutput = 1 + iota
PERMFAIL
TEMPFAIL
NOTSIGNED
TESTINGSUCCESS
TESTINGPERMFAIL
TESTINGTEMPFAIL
)
// sigOptions represents signing options // sigOptions represents signing options
type SigOptions struct { type SigOptions struct {
@@ -98,40 +88,40 @@ func NewSigOptions() SigOptions {
} }
// Sign signs an email // Sign signs an email
func Sign(email *[]byte, options SigOptions) error { func Sign(email []byte, options SigOptions) ([]byte, error) {
var privateKey *rsa.PrivateKey var privateKey *rsa.PrivateKey
// PrivateKey // PrivateKey
if len(options.PrivateKey) == 0 { if len(options.PrivateKey) == 0 {
return ErrSignPrivateKeyRequired return nil, ErrSignPrivateKeyRequired
} }
d, _ := pem.Decode(options.PrivateKey) d, _ := pem.Decode(options.PrivateKey)
key, err := x509.ParsePKCS1PrivateKey(d.Bytes) key, err := x509.ParsePKCS1PrivateKey(d.Bytes)
if err != nil { if err != nil {
return ErrCandNotParsePrivateKey return nil, ErrCandNotParsePrivateKey
} }
privateKey = key privateKey = key
// Domain required // Domain required
if options.Domain == "" { if options.Domain == "" {
return ErrSignDomainRequired return nil, ErrSignDomainRequired
} }
// Selector required // Selector required
if options.Selector == "" { if options.Selector == "" {
return ErrSignSelectorRequired return nil, ErrSignSelectorRequired
} }
// Canonicalization // Canonicalization
options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization)) options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization))
if err != nil { if err != nil {
return err return nil, err
} }
// Algo // Algo
options.Algo = strings.ToLower(options.Algo) options.Algo = strings.ToLower(options.Algo)
if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" { if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" {
return ErrSignBadAlgo return nil, ErrSignBadAlgo
} }
// Header must contain "from" // Header must contain "from"
@@ -144,21 +134,20 @@ func Sign(email *[]byte, options SigOptions) error {
} }
} }
if !hasFrom { if !hasFrom {
return ErrSignHeaderShouldContainsFrom return nil, ErrSignHeaderShouldContainsFrom
} }
// Normalize // Normalize
headers, body, err := canonicalize(email, options.Canonicalization, options.Headers) headers, body, err := canonicalize(email, options.Canonicalization, options.Headers)
if err != nil { if err != nil {
return err return nil, err
} }
signHash := strings.Split(options.Algo, "-") signHash := strings.Split(options.Algo, "-")
// hash body // hash body
bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength) bodyHash, err := getBodyHash(body, signHash[1], options.BodyLength)
if err != nil { if err != nil {
return err return nil, err
} }
// Get dkim header base // Get dkim header base
@@ -168,7 +157,7 @@ func Sign(email *[]byte, options SigOptions) error {
canonicalizations := strings.Split(options.Canonicalization, "/") canonicalizations := strings.Split(options.Canonicalization, "/")
dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0])
if err != nil { if err != nil {
return err return nil, err
} }
headers = append(headers, []byte(dHeaderCanonicalized)...) headers = append(headers, []byte(dHeaderCanonicalized)...)
headers = bytes.TrimRight(headers, " \r\n") headers = bytes.TrimRight(headers, " \r\n")
@@ -189,35 +178,26 @@ func Sign(email *[]byte, options SigOptions) error {
} }
} }
dHeader += subh + CRLF dHeader += subh + CRLF
*email = append([]byte(dHeader), *email...) return append([]byte(dHeader), email...), nil
return nil
} }
// verify verifies an email an return func Verify(email []byte) (dkimHeader *DKIMHeader, err error) {
// state: SUCCESS or PERMFAIL or TEMPFAIL, TESTINGSUCCESS, TESTINGPERMFAIL
// TESTINGTEMPFAIL or NOTSIGNED
// error: if an error occurs during verification
func Verify(email *[]byte) (verifyOutput, error) {
// parse email // parse email
dkimHeader, err := newDkimHeaderFromEmail(email) dkimHeader, err = newDkimHeaderFromEmail(email)
if err != nil { if err != nil {
if err == ErrDkimHeaderNotFound { return
return NOTSIGNED, ErrDkimHeaderNotFound
} else {
return PERMFAIL, err
}
} }
// we do not set query method because if it's others, 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) pubKey, err := newPubKeyFromDnsTxt(dkimHeader.Selector, dkimHeader.Domain)
if err != nil { if err != nil {
return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting) return nil, err
} }
// Normalize // Normalize
headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers)
if err != nil { if err != nil {
return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) return nil, err
} }
sigHash := strings.Split(dkimHeader.Algorithm, "-") sigHash := strings.Split(dkimHeader.Algorithm, "-")
// check if hash algo are compatible // check if hash algo are compatible
@@ -229,60 +209,43 @@ func Verify(email *[]byte) (verifyOutput, error) {
} }
} }
if !compatible { if !compatible {
return getVerifyOutput(PERMFAIL, ErrVerifyInappropriateHashAlgo, pubKey.FlagTesting) return nil, ErrVerifyInappropriateHashAlgo
} }
// expired ? // expired ?
if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() { if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() {
return getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting) return nil, ErrVerifySignatureHasExpired
} }
//println("|" + string(body) + "|") bodyHash, err := getBodyHash(body, sigHash[1], dkimHeader.BodyLength)
// get body hash
bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength)
if err != nil { if err != nil {
return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) return nil, err
} }
//println(bodyHash)
if bodyHash != dkimHeader.BodyHash { if bodyHash != dkimHeader.BodyHash {
return getVerifyOutput(PERMFAIL, ErrVerifyBodyHash, pubKey.FlagTesting) return nil, ErrVerifyBodyHash
} }
// compute sig // compute sig
dkimHeaderCano, err := canonicalizeHeader(dkimHeader.RawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0]) dkimHeaderCano, err := canonicalizeHeader(dkimHeader.RawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0])
if err != nil { if err != nil {
return getVerifyOutput(TEMPFAIL, err, pubKey.FlagTesting) return nil, err
} }
toSignStr := string(headers) + dkimHeaderCano toSignStr := string(headers) + dkimHeaderCano
toSign := bytes.TrimRight([]byte(toSignStr), " \r\n") toSign := bytes.TrimRight([]byte(toSignStr), " \r\n")
err = verifySignature(toSign, dkimHeader.SignatureData, &pubKey.PubKey, sigHash[1]) err = verifySignature(toSign, dkimHeader.SignatureData, &pubKey.PubKey, sigHash[1])
if err != nil { if err != nil {
return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) return nil, err
} }
return SUCCESS, nil if pubKey.FlagTesting {
err = ErrTesting
} }
return
// getVerifyOutput returns output of verify fct according to the testing flag
func getVerifyOutput(status verifyOutput, err error, flagTesting bool) (verifyOutput, error) {
if !flagTesting {
return status, err
}
switch status {
case SUCCESS:
return TESTINGSUCCESS, err
case PERMFAIL:
return TESTINGPERMFAIL, err
case TEMPFAIL:
return TESTINGTEMPFAIL, err
}
// should never happen but compilator sream whithout return
return status, err
} }
// canonicalize returns canonicalized version of header and body // canonicalize returns canonicalized version of header and body
func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, err error) { func canonicalize(email []byte, cano string, h []string) (headers, body []byte, err error) {
body = []byte{} body = []byte{}
rxReduceWS := regexp.MustCompile(`[ \t]+`) rxReduceWS := regexp.MustCompile(`[ \t]+`)
@@ -302,7 +265,6 @@ func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte,
match = nil match = nil
headerToKeepToLower := strings.ToLower(headerToKeep) headerToKeepToLower := strings.ToLower(headerToKeep)
for e := headersList.Front(); e != nil; e = e.Next() { for e := headersList.Front(); e != nil; e = e.Next() {
//fmt.Printf("|%s|\n", e.Value.(string))
t := strings.Split(e.Value.(string), ":") t := strings.Split(e.Value.(string), ":")
if strings.ToLower(t[0]) == headerToKeepToLower { if strings.ToLower(t[0]) == headerToKeepToLower {
match = e match = e
@@ -408,14 +370,14 @@ func canonicalizeHeader(header string, algo string) (string, error) {
} }
// getBodyHash return the hash (bas64encoded) of the body // getBodyHash return the hash (bas64encoded) of the body
func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) { func getBodyHash(body []byte, algo string, bodyLength uint) (string, error) {
var h hash.Hash var h hash.Hash
if algo == "sha1" { if algo == "sha1" {
h = sha1.New() h = sha1.New()
} else { } else {
h = sha256.New() h = sha256.New()
} }
toH := *body toH := body
// if l tag (body length) // if l tag (body length)
if bodyLength != 0 { if bodyLength != 0 {
if uint(len(toH)) < bodyLength { if uint(len(toH)) < bodyLength {
@@ -479,9 +441,10 @@ func verifySignature(toSign []byte, sig64 string, key *rsa.PublicKey, algo strin
return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig) return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig)
} }
var rxReduceWS = regexp.MustCompile(`[ \t]+`)
// 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]+`)
out := strings.Replace(in, "\n", "", -1) out := strings.Replace(in, "\n", "", -1)
out = strings.Replace(out, "\r", "", -1) out = strings.Replace(out, "\r", "", -1)
out = rxReduceWS.ReplaceAllString(out, " ") out = rxReduceWS.ReplaceAllString(out, " ")
@@ -529,9 +492,9 @@ func getHeadersList(rawHeader *[]byte) (*list.List, error) {
} }
// getHeadersBody return headers and body // getHeadersBody return headers and body
func getHeadersBody(email *[]byte) ([]byte, []byte, error) { func getHeadersBody(email []byte) ([]byte, []byte, error) {
// TODO: \n -> \r\n // TODO: \n -> \r\n
parts := bytes.SplitN(*email, []byte{13, 10, 13, 10}, 2) parts := bytes.SplitN(email, []byte{13, 10, 13, 10}, 2)
if len(parts) != 2 { if len(parts) != 2 {
return []byte{}, []byte{}, ErrBadMailFormat return []byte{}, []byte{}, ErrBadMailFormat
} }

View File

@@ -10,7 +10,8 @@ import (
"time" "time"
) )
type dkimHeader struct { // DKIMHeader
type DKIMHeader struct {
// Version This tag defines the version of DKIM // Version This tag defines the version of DKIM
// specification that applies to the signature record. // specification that applies to the signature record.
// tag v // tag v
@@ -99,7 +100,7 @@ type dkimHeader struct {
// Internationalized domain names MUST be encoded as A-labels, as // Internationalized domain names MUST be encoded as A-labels, as
// described in Section 2.3 of [RFC5890]. // described in Section 2.3 of [RFC5890].
// tag i // tag i
Auid string AUID string
// Body length count (plain-text unsigned decimal integer; OPTIONAL, // Body length count (plain-text unsigned decimal integer; OPTIONAL,
// default is entire body). This tag informs the Verifier of the // default is entire body). This tag informs the Verifier of the
@@ -197,14 +198,14 @@ type dkimHeader struct {
} }
// NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value // NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value
func newDkimHeaderBySigOptions(options SigOptions) *dkimHeader { func newDkimHeaderBySigOptions(options SigOptions) *DKIMHeader {
h := new(dkimHeader) h := new(DKIMHeader)
h.Version = "1" h.Version = "1"
h.Algorithm = options.Algo h.Algorithm = options.Algo
h.MessageCanonicalization = options.Canonicalization h.MessageCanonicalization = options.Canonicalization
h.Domain = options.Domain h.Domain = options.Domain
h.Headers = options.Headers h.Headers = options.Headers
h.Auid = options.Auid h.AUID = options.Auid
h.BodyLength = options.BodyLength h.BodyLength = options.BodyLength
h.QueryMethods = options.QueryMethods h.QueryMethods = options.QueryMethods
h.Selector = options.Selector h.Selector = options.Selector
@@ -221,8 +222,8 @@ func newDkimHeaderBySigOptions(options SigOptions) *dkimHeader {
// NewFromEmail return a new DkimHeader by parsing an email // NewFromEmail return a new DkimHeader by parsing an email
// Note: according to RFC 6376 an email can have multiple DKIM Header // Note: according to RFC 6376 an email can have multiple DKIM Header
// in this case we return the last inserted or the last with d== mail from // in this case we return the last inserted or the last with d== mail from
func newDkimHeaderFromEmail(email *[]byte) (*dkimHeader, error) { func newDkimHeaderFromEmail(email []byte) (*DKIMHeader, error) {
m, err := mail.ReadMessage(bytes.NewReader(*email)) m, err := mail.ReadMessage(bytes.NewReader(email))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -265,7 +266,7 @@ func newDkimHeaderFromEmail(email *[]byte) (*dkimHeader, error) {
} }
} }
var keep *dkimHeader var keep *DKIMHeader
var keepErr error var keepErr error
//for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] { //for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] {
for _, h := range dkHeaders { for _, h := range dkHeaders {
@@ -291,8 +292,8 @@ func newDkimHeaderFromEmail(email *[]byte) (*dkimHeader, error) {
} }
// parseDkHeader parse raw dkim header // 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)
keyVal := strings.SplitN(header, ":", 2) keyVal := strings.SplitN(header, ":", 2)
@@ -384,7 +385,7 @@ func parseDkHeader(header string) (dkh *dkimHeader, err error) {
if !strings.HasSuffix(data, dkh.Domain) { if !strings.HasSuffix(data, dkh.Domain) {
return nil, ErrDkimHeaderDomainMismatch return nil, ErrDkimHeaderDomainMismatch
} }
dkh.Auid = data dkh.AUID = data
} }
case "l": case "l":
ui, err := strconv.ParseUint(data, 10, 32) ui, err := strconv.ParseUint(data, 10, 32)
@@ -423,8 +424,8 @@ func parseDkHeader(header string) (dkh *dkimHeader, err error) {
} }
// default for i/Auid // default for i/Auid
if dkh.Auid == "" { if dkh.AUID == "" {
dkh.Auid = "@" + dkh.Domain dkh.AUID = "@" + dkh.Domain
} }
// defaut for query method // defaut for query method
@@ -438,7 +439,7 @@ func parseDkHeader(header string) (dkh *dkimHeader, err error) {
// GetHeaderBase return base header for signers // GetHeaderBase return base header for signers
// Todo: some refactoring needed... // Todo: some refactoring needed...
func (d *dkimHeader) getHeaderBaseForSigning(bodyHash string) string { func (d *DKIMHeader) getHeaderBaseForSigning(bodyHash string) string {
h := "DKIM-Signature: v=" + d.Version + "; a=" + d.Algorithm + "; q=" + strings.Join(d.QueryMethods, ":") + "; c=" + d.MessageCanonicalization + ";" + CRLF + TAB h := "DKIM-Signature: v=" + d.Version + "; a=" + d.Algorithm + "; q=" + strings.Join(d.QueryMethods, ":") + "; c=" + d.MessageCanonicalization + ";" + CRLF + TAB
subh := "s=" + d.Selector + ";" subh := "s=" + d.Selector + ";"
if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength { if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength {
@@ -448,12 +449,12 @@ func (d *dkimHeader) getHeaderBaseForSigning(bodyHash string) string {
subh += " d=" + d.Domain + ";" subh += " d=" + d.Domain + ";"
// Auid // Auid
if len(d.Auid) != 0 { if len(d.AUID) != 0 {
if len(subh)+len(d.Auid)+4 > MaxHeaderLineLength { if len(subh)+len(d.AUID)+4 > MaxHeaderLineLength {
h += subh + FWS h += subh + FWS
subh = "" subh = ""
} }
subh += " i=" + d.Auid + ";" subh += " i=" + d.AUID + ";"
} }
/*h := "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tmail.io; i=@tmail.io;" + FWS /*h := "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tmail.io; i=@tmail.io;" + FWS

View File

@@ -212,57 +212,57 @@ func Test_SignConfig(t *testing.T) {
email := []byte(emailBase) email := []byte(emailBase)
emailToTest := append([]byte(nil), email...) emailToTest := append([]byte(nil), email...)
options := NewSigOptions() options := NewSigOptions()
err := Sign(&emailToTest, options) _, err := Sign(emailToTest, options)
assert.NotNil(t, err) assert.NotNil(t, err)
// && err No private key // && err No private key
assert.EqualError(t, err, ErrSignPrivateKeyRequired.Error()) assert.EqualError(t, err, ErrSignPrivateKeyRequired.Error())
options.PrivateKey = []byte(privKey) options.PrivateKey = []byte(privKey)
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
err = Sign(&emailToTest, options) _, err = Sign(emailToTest, options)
// Domain // Domain
assert.EqualError(t, err, ErrSignDomainRequired.Error()) assert.EqualError(t, err, ErrSignDomainRequired.Error())
options.Domain = "toorop.fr" options.Domain = "toorop.fr"
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
err = Sign(&emailToTest, options) _, err = Sign(emailToTest, options)
// Selector // Selector
assert.Error(t, err, ErrSignSelectorRequired.Error()) assert.Error(t, err, ErrSignSelectorRequired.Error())
options.Selector = "default" options.Selector = "default"
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
err = Sign(&emailToTest, options) _, err = Sign(emailToTest, options)
assert.NoError(t, err) assert.NoError(t, err)
// Canonicalization // Canonicalization
options.Canonicalization = "simple/relaxed/simple" options.Canonicalization = "simple/relaxed/simple"
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
err = Sign(&emailToTest, options) _, err = Sign(emailToTest, options)
assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) assert.EqualError(t, err, ErrSignBadCanonicalization.Error())
options.Canonicalization = "simple/relax" options.Canonicalization = "simple/relax"
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
err = Sign(&emailToTest, options) _, err = Sign(emailToTest, options)
assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) assert.EqualError(t, err, ErrSignBadCanonicalization.Error())
options.Canonicalization = "relaxed" options.Canonicalization = "relaxed"
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
err = Sign(&emailToTest, options) _, err = Sign(emailToTest, options)
assert.NoError(t, err) assert.NoError(t, err)
options.Canonicalization = "SiMple/relAxed" options.Canonicalization = "SiMple/relAxed"
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
err = Sign(&emailToTest, options) _, err = Sign(emailToTest, options)
assert.NoError(t, err) assert.NoError(t, err)
// header // header
options.Headers = []string{"toto"} options.Headers = []string{"toto"}
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
err = Sign(&emailToTest, options) _, err = Sign(emailToTest, options)
assert.EqualError(t, err, ErrSignHeaderShouldContainsFrom.Error()) assert.EqualError(t, err, ErrSignHeaderShouldContainsFrom.Error())
options.Headers = []string{"To", "From"} options.Headers = []string{"To", "From"}
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
err = Sign(&emailToTest, options) _, err = Sign(emailToTest, options)
assert.NoError(t, err) assert.NoError(t, err)
} }
@@ -274,18 +274,18 @@ func Test_canonicalize(t *testing.T) {
options.Headers = []string{"from", "date", "mime-version", "received", "received", "In-Reply-To"} options.Headers = []string{"from", "date", "mime-version", "received", "received", "In-Reply-To"}
// simple/simple // simple/simple
options.Canonicalization = "simple/simple" options.Canonicalization = "simple/simple"
header, body, err := canonicalize(&emailToTest, options.Canonicalization, options.Headers) header, body, err := canonicalize(emailToTest, options.Canonicalization, options.Headers)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []byte(headerSimple), header) assert.Equal(t, headerSimple, string(header))
assert.Equal(t, []byte(bodySimple), body) assert.Equal(t, bodySimple, string(body))
// relaxed/relaxed // relaxed/relaxed
emailToTest = append([]byte(nil), email...) emailToTest = append([]byte(nil), email...)
options.Canonicalization = "relaxed/relaxed" options.Canonicalization = "relaxed/relaxed"
header, body, err = canonicalize(&emailToTest, options.Canonicalization, options.Headers) header, body, err = canonicalize(emailToTest, options.Canonicalization, options.Headers)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []byte(headerRelaxed), header) assert.Equal(t, headerRelaxed, string(header))
assert.Equal(t, []byte(bodyRelaxed), body) assert.Equal(t, bodyRelaxed, string(body))
} }
@@ -301,77 +301,70 @@ func Test_Sign(t *testing.T) {
options.AddSignatureTimestamp = false options.AddSignatureTimestamp = false
options.Canonicalization = "relaxed/relaxed" options.Canonicalization = "relaxed/relaxed"
err := Sign(&emailRelaxed, options) emailRelaxed, err := Sign(emailRelaxed, options)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []byte(signedRelaxedRelaxed), emailRelaxed) assert.Equal(t, signedRelaxedRelaxed, string(emailRelaxed))
options.BodyLength = 5 options.BodyLength = 5
emailRelaxed = append([]byte(nil), email...) emailRelaxed = append([]byte(nil), email...)
err = Sign(&emailRelaxed, options) emailRelaxed, err = Sign(emailRelaxed, options)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []byte(signedRelaxedRelaxedLength), emailRelaxed) assert.Equal(t, signedRelaxedRelaxedLength, string(emailRelaxed))
options.BodyLength = 0 options.BodyLength = 0
options.Canonicalization = "simple/simple" options.Canonicalization = "simple/simple"
emailSimple := append([]byte(nil), email...) emailSimple := append([]byte(nil), email...)
err = Sign(&emailSimple, options) emailSimple, err = Sign(emailSimple, options)
assert.Equal(t, []byte(signedSimpleSimple), emailSimple) assert.Equal(t, signedSimpleSimple, string(emailSimple))
options.Headers = []string{"from", "subject", "date", "message-id"} options.Headers = []string{"from", "subject", "date", "message-id"}
memail := []byte(missingHeaderMail) memail := []byte(missingHeaderMail)
err = Sign(&memail, options) _, err = Sign(memail, options)
assert.NoError(t, err) assert.NoError(t, err)
options.BodyLength = 5 options.BodyLength = 5
options.Canonicalization = "simple/simple" options.Canonicalization = "simple/simple"
emailSimple = append([]byte(nil), email...) emailSimple = append([]byte(nil), email...)
err = Sign(&emailSimple, options) emailSimple, err = Sign(emailSimple, options)
assert.Equal(t, []byte(signedSimpleSimpleLength), emailSimple) assert.Equal(t, signedSimpleSimpleLength, string(emailSimple))
} }
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, err := Verify(&email) _, err := Verify(email)
assert.Equal(t, NOTSIGNED, status)
assert.Equal(t, ErrDkimHeaderNotFound, err) assert.Equal(t, ErrDkimHeaderNotFound, err)
// No From // No From
email = []byte(signedNoFrom) email = []byte(signedNoFrom)
status, err = Verify(&email) _, err = Verify(email)
assert.Equal(t, ErrVerifyBodyHash, err) assert.Equal(t, ErrVerifyBodyHash, err)
assert.Equal(t, TESTINGPERMFAIL, 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, err = Verify(&email) _, err = Verify(email)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, PERMFAIL, status)
assert.Equal(t, ErrDkimHeaderMissingRequiredTag, err) assert.Equal(t, ErrDkimHeaderMissingRequiredTag, err)
// missing bad algo // missing bad algo
email = []byte(signedBadAlgo) email = []byte(signedBadAlgo)
status, err = Verify(&email) _, err = Verify(email)
assert.Equal(t, PERMFAIL, status)
assert.Equal(t, ErrSignBadAlgo, err) assert.Equal(t, ErrSignBadAlgo, err)
// relaxed // relaxed
email = []byte(signedRelaxedRelaxedLength) email = []byte(signedRelaxedRelaxedLength)
status, err = Verify(&email) _, err = Verify(email)
assert.NoError(t, err) assert.Equal(t, ErrTesting, err)
assert.Equal(t, SUCCESS, status)
// simple // simple
email = []byte(signedSimpleSimpleLength) email = []byte(signedSimpleSimpleLength)
status, err = Verify(&email) _, err = Verify(email)
assert.NoError(t, err) assert.Equal(t, ErrTesting, err)
assert.Equal(t, SUCCESS, status)
// gmail // gmail
email = []byte(fromGmail) email = []byte(fromGmail)
status, err = Verify(&email) _, err = Verify(email)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, SUCCESS, status)
} }

View File

@@ -62,7 +62,8 @@ var (
// ErrVerifyNoKeyForSignature // ErrVerifyNoKeyForSignature
ErrVerifyNoKeyForSignature = errors.New("no key for verify") ErrVerifyNoKeyForSignature = errors.New("no key for verify")
// ErrVerifyKeyUnavailable when service (dns) is anavailable // ErrVerifyKeyUnavailable when service (dns) is anavailable.
// This error may be temporary in some cases.
ErrVerifyKeyUnavailable = errors.New("key unavailable") ErrVerifyKeyUnavailable = errors.New("key unavailable")
// ErrVerifyTagVMustBeTheFirst if present the v tag must be the firts in the record // ErrVerifyTagVMustBeTheFirst if present the v tag must be the firts in the record
@@ -88,4 +89,7 @@ var (
// ErrVerifyInappropriateHashAlgo when h tag in pub key doesn't contain hash algo from a tag of DKIM header // ErrVerifyInappropriateHashAlgo when h tag in pub key doesn't contain hash algo from a tag of DKIM header
ErrVerifyInappropriateHashAlgo = errors.New("inappropriate has algorithm") ErrVerifyInappropriateHashAlgo = errors.New("inappropriate has algorithm")
// ErrTesting
ErrTesting = errors.New("public key has testing flag set")
) )

View File

@@ -20,19 +20,19 @@ type pubKeyRep struct {
FlagIMustBeD bool // flag i FlagIMustBeD bool // flag i
} }
func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, error) { func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, error) {
txt, err := net.LookupTXT(selector + "._domainkey." + domain) txt, err := net.LookupTXT(selector + "._domainkey." + domain)
if err != nil { if err != nil {
if strings.HasSuffix(err.Error(), "no such host") { if strings.HasSuffix(err.Error(), "no such host") {
return nil, PERMFAIL, ErrVerifyNoKeyForSignature return nil, ErrVerifyNoKeyForSignature
} else { } else {
return nil, TEMPFAIL, ErrVerifyKeyUnavailable return nil, ErrVerifyKeyUnavailable
} }
} }
// empty record // empty record
if len(txt) == 0 { if len(txt) == 0 {
return nil, PERMFAIL, ErrVerifyNoKeyForSignature return nil, ErrVerifyNoKeyForSignature
} }
pkr := new(pubKeyRep) pkr := new(pubKeyRep)
@@ -57,11 +57,11 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err
case "v": case "v":
// RFC: is this tag is specified it MUST be the first in the record // RFC: is this tag is specified it MUST be the first in the record
if i != 0 { if i != 0 {
return nil, PERMFAIL, ErrVerifyTagVMustBeTheFirst return nil, ErrVerifyTagVMustBeTheFirst
} }
pkr.Version = val pkr.Version = val
if pkr.Version != "DKIM1" { if pkr.Version != "DKIM1" {
return nil, PERMFAIL, ErrVerifyVersionMusBeDkim1 return nil, ErrVerifyVersionMusBeDkim1
} }
case "h": case "h":
p := strings.Split(strings.ToLower(val), ":") p := strings.Split(strings.ToLower(val), ":")
@@ -78,18 +78,18 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err
} }
case "k": case "k":
if strings.ToLower(val) != "rsa" { if strings.ToLower(val) != "rsa" {
return nil, PERMFAIL, ErrVerifyBadKeyType return nil, ErrVerifyBadKeyType
} }
case "n": case "n":
pkr.Note = val pkr.Note = val
case "p": case "p":
rawkey := val rawkey := val
if rawkey == "" { if rawkey == "" {
return nil, PERMFAIL, ErrVerifyRevokedKey return nil, ErrVerifyRevokedKey
} }
un64, err := base64.StdEncoding.DecodeString(rawkey) un64, err := base64.StdEncoding.DecodeString(rawkey)
if err != nil { if err != nil {
return nil, PERMFAIL, ErrVerifyBadKey return nil, ErrVerifyBadKey
} }
pk, err := x509.ParsePKIXPublicKey(un64) pk, err := x509.ParsePKIXPublicKey(un64)
pkr.PubKey = *pk.(*rsa.PublicKey) pkr.PubKey = *pk.(*rsa.PublicKey)
@@ -120,8 +120,8 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err
// if no pubkey // if no pubkey
if pkr.PubKey == (rsa.PublicKey{}) { if pkr.PubKey == (rsa.PublicKey{}) {
return nil, PERMFAIL, ErrVerifyNoKey return nil, ErrVerifyNoKey
} }
return pkr, SUCCESS, nil return pkr, nil
} }