This commit is contained in:
Stéphane Depierrepont aka Toorop
2015-05-11 18:32:19 +02:00
parent f9b2a6291f
commit d652e23632
4 changed files with 328 additions and 28 deletions

91
dkim.go
View File

@@ -68,14 +68,14 @@ type sigOptions struct {
SignatureExpireIn uint64 SignatureExpireIn uint64
// CopiedHeaderFileds // CopiedHeaderFileds
CopiedHeaderFileds []string CopiedHeaderFields []string
} }
// NewSigOption returns new sigoption with some defaults value // NewSigOption returns new sigoption with some defaults value
func NewSigOptions() sigOptions { func NewSigOptions() sigOptions {
return sigOptions{ return sigOptions{
Version: 1, Version: 1,
Canonicalization: "relaxed/simple", Canonicalization: "simple/simple",
Algo: "rsa-sha256", Algo: "rsa-sha256",
Headers: []string{"from"}, Headers: []string{"from"},
BodyLength: 0, BodyLength: 0,
@@ -111,18 +111,9 @@ func Sign(email *[]byte, options sigOptions) error {
} }
// Canonicalization // Canonicalization
options.Canonicalization = strings.ToLower(options.Canonicalization) options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization))
p := strings.Split(options.Canonicalization, "/") if err != nil {
if len(p) > 2 { return err
return ErrSignBadCanonicalization
}
if len(p) == 1 {
options.Canonicalization = options.Canonicalization + "/simple"
}
for _, c := range p {
if c != "simple" && c != "relaxed" {
return ErrSignBadCanonicalization
}
} }
// Algo // Algo
@@ -146,7 +137,7 @@ func Sign(email *[]byte, options sigOptions) error {
} }
// Normalize // Normalize
headers, body, err := canonicalize(email, options) headers, body, err := canonicalize(email, options.Canonicalization, options.Headers)
if err != nil { if err != nil {
return err return err
} }
@@ -179,7 +170,7 @@ func Sign(email *[]byte, options sigOptions) error {
// Get dkim header base // Get dkim header base
dkimHeader := NewDkimHeaderBySigOptions(options) dkimHeader := NewDkimHeaderBySigOptions(options)
dHeader := dkimHeader.GetHeaderBase(bodyHash) dHeader := dkimHeader.GetHeaderBaseForSigning(bodyHash)
canonicalizations := strings.Split(options.Canonicalization, "/") canonicalizations := strings.Split(options.Canonicalization, "/")
dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0])
@@ -214,8 +205,35 @@ func Sign(email *[]byte, options sigOptions) error {
return nil return nil
} }
// verify verifies an email an return
// 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) {
// parse email
dkimHeader, err := NewFromEmail(email)
if err != nil {
if err == ErrDkimHeaderNotFound {
return "NOTSIGNED", ErrDkimHeaderNotFound.Error(), nil
} else {
return "PERMFAIL", err.Error(), err
}
}
println(dkimHeader)
// // Normalize
headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers)
if err != nil {
return "PERMFAIL", err.Error(), err
}
// HERE
return "SUCCESS", "", nil
}
// canonicalize returns canonicalized version of header and body // canonicalize returns canonicalized version of header and body
func canonicalize(email *[]byte, options sigOptions) (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]+`)
@@ -231,7 +249,7 @@ func canonicalize(email *[]byte, options sigOptions) (headers, body []byte, err
parts[1] = []byte{13, 10} parts[1] = []byte{13, 10}
} }
canonicalizations := strings.Split(options.Canonicalization, "/") canonicalizations := strings.Split(cano, "/")
// canonicalyze header // canonicalyze header
headersList := list.New() headersList := list.New()
@@ -258,7 +276,7 @@ func canonicalize(email *[]byte, options sigOptions) (headers, body []byte, err
var match *list.Element var match *list.Element
headersToKeepList := list.New() headersToKeepList := list.New()
for _, headerToKeep := range options.Headers { for _, headerToKeep := range h {
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() {
@@ -323,7 +341,7 @@ func canonicalize(email *[]byte, options sigOptions) (headers, body []byte, err
// canonicalizeHeader returns canonicalized version of header // canonicalizeHeader returns canonicalized version of header
func canonicalizeHeader(header string, algo string) (string, error) { func canonicalizeHeader(header string, algo string) (string, error) {
rxReduceWS := regexp.MustCompile(`[ \t]+`) //rxReduceWS := regexp.MustCompile(`[ \t]+`)
if algo == "simple" { if algo == "simple" {
// The "simple" header canonicalization algorithm does not change header // The "simple" header canonicalization algorithm does not change header
// fields in any way. Header fields MUST be presented to the signing or // fields in any way. Header fields MUST be presented to the signing or
@@ -360,11 +378,36 @@ func canonicalizeHeader(header string, algo string) (string, error) {
} }
k := strings.ToLower(kv[0]) k := strings.ToLower(kv[0])
k = strings.TrimSpace(k) k = strings.TrimSpace(k)
v := strings.Replace(kv[1], "\n", "", -1) v := removeFWS(kv[1])
v = strings.Replace(v, "\r", "", -1) //v = rxReduceWS.ReplaceAllString(v, " ")
v = rxReduceWS.ReplaceAllString(v, " ") //v = strings.TrimSpace(v)
v = strings.TrimSpace(v)
return k + ":" + v + CRLF, nil return k + ":" + v + CRLF, nil
} }
return header, ErrSignBadCanonicalization return header, ErrSignBadCanonicalization
} }
// removeFWS removes all FWS from string
func removeFWS(in string) string {
rxReduceWS := regexp.MustCompile(`[ \t]+`)
out := strings.Replace(in, "\n", "", -1)
out = strings.Replace(out, "\r", "", -1)
out = rxReduceWS.ReplaceAllString(out, " ")
return strings.TrimSpace(out)
}
// validateCanonicalization validate canonicalization (c flag)
func validateCanonicalization(cano string) (string, error) {
p := strings.Split(cano, "/")
if len(p) > 2 {
return "", ErrSignBadCanonicalization
}
if len(p) == 1 {
cano = cano + "/simple"
}
for _, c := range p {
if c != "simple" && c != "relaxed" {
return "", ErrSignBadCanonicalization
}
}
return cano, nil
}

View File

@@ -1,7 +1,12 @@
package dkim package dkim
import ( import (
"bytes"
"errors"
"fmt" "fmt"
"net/mail"
"net/textproto"
"strconv"
"strings" "strings"
"time" "time"
) )
@@ -180,7 +185,16 @@ type DkimHeader struct {
// in the "z=" tag. Copied header field values are for diagnostic // in the "z=" tag. Copied header field values are for diagnostic
// use. // use.
// tag z // tag z
CopiedHeaderFileds []string CopiedHeaderFields []string
// HeaderMailFromDomain store the raw email address of the header Mail From
// used for verifying in case of multiple DKIM header (we will prioritise
// header with d = mail from domain)
//HeaderMailFromDomain string
// RawForsign represents the raw part (with non canonicalization) of the header
// used for computint sig in verify process
RawForSign string
} }
// NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value // NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value
@@ -201,13 +215,165 @@ func NewDkimHeaderBySigOptions(options sigOptions) *DkimHeader {
if options.SignatureExpireIn > 0 { if options.SignatureExpireIn > 0 {
h.SignatureExpiration = time.Now().Add(time.Duration(options.SignatureExpireIn) * time.Second) h.SignatureExpiration = time.Now().Add(time.Duration(options.SignatureExpireIn) * time.Second)
} }
h.CopiedHeaderFileds = options.CopiedHeaderFileds h.CopiedHeaderFields = options.CopiedHeaderFields
return h return h
} }
// NewFromEmail return a new DkimHeader by parsing an email
// 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
func NewFromEmail(email *[]byte) (*DkimHeader, error) {
m, err := mail.ReadMessage(bytes.NewReader(*email))
if err != nil {
return nil, err
}
// DKIM header ?
if len(m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")]) == 0 {
return nil, ErrDkimHeaderNotFound
}
// Get mail from domain
mailFromDomain := ""
mailfrom, err := mail.ParseAddress(m.Header.Get(textproto.CanonicalMIMEHeaderKey("From")))
if err != nil {
if err.Error() != "mail: no address" {
return nil, err
}
} else {
t := strings.SplitAfter(mailfrom.Address, "@")
if len(t) > 1 {
mailFromDomain = strings.ToLower(t[1])
}
}
var keep *DkimHeader
var keepErr error
for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] {
parsed, err := parseDkHeader(dk)
// if malformed dkim header try next
if err != nil {
keepErr = err
continue
}
// Keep first dkim headers
if keep == nil {
keep = parsed
}
// if d flag == domain keep this header and return
if mailFromDomain == parsed.Domain {
return parsed, nil
}
}
if keep == nil {
return nil, keepErr
}
return keep, nil
}
func parseDkHeader(header string) (dkh *DkimHeader, err error) {
dkh = new(DkimHeader)
t := strings.LastIndex(header, "b=")
if t == -1 {
return nil, ErrDkimHeaderBTagNotFound
}
dkh.RawForSign = header[0 : t+2]
// Mandatory
mandatoryFlags := make(map[string]bool, 7) //(b'v', b'a', b'b', b'bh', b'd', b'h', b's')
mandatoryFlags["v"] = false
mandatoryFlags["a"] = false
mandatoryFlags["b"] = false
mandatoryFlags["bh"] = false
mandatoryFlags["d"] = false
mandatoryFlags["h"] = false
mandatoryFlags["s"] = false
// default values
dkh.MessageCanonicalization = "simple/simple"
dkh.QueryMethods = []string{"dns/txt"}
fs := strings.Split(header, ";")
for _, f := range fs {
flagData := strings.SplitN(f, "=", 2)
flag := strings.ToLower(strings.TrimSpace(flagData[0]))
data := strings.TrimSpace(flagData[1])
switch flag {
case "v":
if data != "1" {
return nil, ErrDkimVersionUnsuported
}
dkh.Version = data
mandatoryFlags["v"] = true
case "a":
dkh.Algorithm = strings.ToLower(data)
if dkh.Algorithm != "rsa-sha1" && dkh.Algorithm != "rsa-sha256" {
return nil, ErrSignBadAlgo
}
mandatoryFlags["a"] = true
case "b":
dkh.SignatureData = removeFWS(data)
mandatoryFlags["b"] = true
case "bh":
dkh.BodyHash = removeFWS(data)
mandatoryFlags["bh"] = true
case "d":
dkh.Domain = strings.ToLower(data)
mandatoryFlags["d"] = true
case "h":
data = strings.ToLower(data)
dkh.Headers = strings.Split(data, ":")
mandatoryFlags["h"] = true
case "s":
dkh.Selector = strings.ToLower(data)
mandatoryFlags["s"] = true
case "c":
dkh.MessageCanonicalization, err = validateCanonicalization(strings.ToLower(data))
if err != nil {
return
}
case "i":
dkh.Auid = data
case "l":
ui, err := strconv.ParseUint(data, 10, 32)
if err != nil {
return nil, err
}
dkh.BodyLength = uint(ui)
case "q":
dkh.QueryMethods = strings.Split(data, ":")
case "t":
ts, err := strconv.ParseInt(data, 10, 64)
if err != nil {
return nil, err
}
dkh.SignatureTimestamp = time.Unix(ts, 0)
case "x":
ts, err := strconv.ParseInt(data, 10, 64)
if err != nil {
return nil, err
}
dkh.SignatureExpiration = time.Unix(ts, 0)
case "z":
dkh.CopiedHeaderFields = strings.Split(data, "|")
}
}
// All mandatory flags are in ?
for f, p := range mandatoryFlags {
if !p {
return nil, errors.New("missing '" + f + "' flag in DKIM header")
}
}
return dkh, nil
}
// GetHeaderBase return base header for signers // GetHeaderBase return base header for signers
// Todo: some refactoring needed... // Todo: some refactoring needed...
func (d *DkimHeader) GetHeaderBase(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 {

View File

@@ -52,6 +52,23 @@ var emailBase = "Received: (qmail 28277 invoked from network); 1 May 2015 09:43:
"-- " + CRLF + "-- " + CRLF +
"Toorop" + CRLF + CRLF + CRLF + CRLF + CRLF + CRLF "Toorop" + CRLF + CRLF + CRLF + CRLF + CRLF + CRLF
var emailBaseNoFrom = "Received: (qmail 28277 invoked from network); 1 May 2015 09:43:37 -0000" + CRLF +
"Received: (qmail 21323 invoked from network); 1 May 2015 09:48:39 -0000" + CRLF +
"Received: from mail483.ha.ovh.net (b6.ovh.net [213.186.33.56])" + CRLF +
" by mo51.mail-out.ovh.net (Postfix) with SMTP id A6E22FF8934" + CRLF +
" for <toorop@toorop.fr>; Mon, 4 May 2015 14:00:47 +0200 (CEST)" + CRLF +
"MIME-Version: 1.0" + CRLF +
"Date: Fri, 1 May 2015 11:48:37 +0200" + CRLF +
"Message-ID: <CADu37kTXBeNkJdXc4bSF8DbJnXmNjkLbnswK6GzG_2yn7U7P6w@tmail.io>" + CRLF +
"Subject: Test DKIM" + CRLF +
"To: =?UTF-8?Q?St=C3=A9phane_Depierrepont?= <toorop@toorop.fr>" + CRLF +
"Content-Type: text/plain; charset=UTF-8" + CRLF + CRLF +
"Hello world" + CRLF +
"line with trailing space " + CRLF +
"line with space " + CRLF +
"-- " + CRLF +
"Toorop" + CRLF + CRLF + CRLF + CRLF + CRLF + CRLF
var headerSimple = "From: =?UTF-8?Q?St=C3=A9phane_Depierrepont?= <toorop@tmail.io>" + CRLF + var headerSimple = "From: =?UTF-8?Q?St=C3=A9phane_Depierrepont?= <toorop@tmail.io>" + CRLF +
"Date: Fri, 1 May 2015 11:48:37 +0200" + CRLF + "Date: Fri, 1 May 2015 11:48:37 +0200" + CRLF +
"MIME-Version: 1.0" + CRLF + "MIME-Version: 1.0" + CRLF +
@@ -94,10 +111,44 @@ var signedSimpleSimple = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=simple
" AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF + " AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF +
" DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBase " 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 +
" bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF +
" b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF +
" AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF +
" DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBaseNoFrom
var signedMissingFlag = "DKIM-Signature: v=1; q=dns/txt; c=simple/simple;" + CRLF +
" s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF +
" bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF +
" b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF +
" AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF +
" DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBase
var signedBadAlgo = "DKIM-Signature: v=1; a=rsa-shasha; q=dns/txt; c=simple/simple;" + CRLF +
" s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF +
" bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF +
" b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF +
" AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF +
" DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBase
var signedDouble = "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 +
" bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF +
" b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF +
" AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF +
" DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF +
"DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed;" + CRLF +
" s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF +
" bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF +
" b=byhiFWd0lAM1sqD1tl8S1DZtKNqgiEZp8jrGds6RRydnZkdX9rCPeL0Q5MYWBQ/JmQrml5" + CRLF +
" pIghLwl/EshDBmNy65O6qO8pSSGgZmM3T7SRLMloex8bnrBJ4KSYcHV46639gVEWcBOKW0" + CRLF +
" h1djZu2jaTuxGeJzlFVtw3Arf2B93cc=" + CRLF + emailBase
func Test_NewSigOptions(t *testing.T) { func Test_NewSigOptions(t *testing.T) {
options := NewSigOptions() options := NewSigOptions()
assert.Equal(t, "rsa-sha256", options.Algo) assert.Equal(t, "rsa-sha256", options.Algo)
assert.Equal(t, "relaxed/simple", options.Canonicalization) assert.Equal(t, "simple/simple", options.Canonicalization)
} }
/*func Test_SignConfig(t *testing.T) { /*func Test_SignConfig(t *testing.T) {
@@ -203,3 +254,32 @@ func Test_Sign(t *testing.T) {
assert.Equal(t, []byte(signedSimpleSimple), emailSimple) assert.Equal(t, []byte(signedSimpleSimple), emailSimple)
} }
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)
// No From
email = []byte(signedNoFrom)
status, msg, err = Verify(&email)
assert.NoError(t, err)
assert.Equal(t, "SUCCESS", status)
// missing mandatory 'a' flag
email = []byte(signedMissingFlag)
status, msg, err = Verify(&email)
assert.Error(t, err)
assert.Equal(t, "PERMFAIL", status)
assert.Equal(t, "missing 'a' flag in DKIM header", msg)
// 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)
}

View File

@@ -34,4 +34,15 @@ var (
// ErrBadDKimTagLBodyTooShort // ErrBadDKimTagLBodyTooShort
ErrBadDKimTagLBodyTooShort = errors.New("bad tag l or bodyLength option. Body length < l value") ErrBadDKimTagLBodyTooShort = errors.New("bad tag l or bodyLength option. Body length < l value")
// ErrDkimHeaderNotFound when there's no DKIM-Signature header in an email we have to verify
ErrDkimHeaderNotFound = errors.New("no DKIM-Signature header field found ")
// 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")
// Version not supported
ErrDkimVersionUnsuported = errors.New("unsuported DKIM version")
) )