sign implemted (need real life testing)

This commit is contained in:
Stéphane Depierrepont aka Toorop
2015-05-05 12:12:57 +02:00
parent f251c6aa7e
commit c27b209a79
3 changed files with 263 additions and 55 deletions

193
dkim.go
View File

@@ -3,6 +3,15 @@ package dkim
import ( import (
"bytes" "bytes"
"container/list" "container/list"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"hash"
"io/ioutil" "io/ioutil"
"regexp" "regexp"
"strings" "strings"
@@ -11,6 +20,9 @@ import (
const ( const (
CRLF = "\r\n" CRLF = "\r\n"
TAB = "\t"
FWS = CRLF + TAB
MaxHeaderLineLength = 70
) )
// sigOptions represents signing options // sigOptions represents signing options
@@ -54,6 +66,9 @@ type sigOptions struct {
// Time validity of the signature (0=never) // Time validity of the signature (0=never)
SignatureExpireIn time.Duration SignatureExpireIn time.Duration
// CopiedHeaderFileds
CopiedHeaderFileds []string
} }
// NewSigOption returns new sigoption with some defaults value // NewSigOption returns new sigoption with some defaults value
@@ -72,6 +87,7 @@ func NewSigOptions() sigOptions {
// Sign signs an email // Sign signs an email
func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) { func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) {
var privateKey *rsa.PrivateKey
// check && sanitize config // check && sanitize config
// PrivateKey (required & TODO: valid) // PrivateKey (required & TODO: valid)
@@ -79,6 +95,13 @@ func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) {
return nil, ErrSignPrivateKeyRequired return nil, ErrSignPrivateKeyRequired
} }
d, _ := pem.Decode([]byte(options.PrivateKey))
key, err := x509.ParsePKCS1PrivateKey(d.Bytes)
if err != nil {
return nil, err
}
privateKey = key
// Domain required // Domain required
if options.Domain == "" { if options.Domain == "" {
return nil, ErrSignDomainRequired return nil, ErrSignDomainRequired
@@ -125,13 +148,74 @@ func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) {
} }
// Normalize // Normalize
//normalizedHeaders, NormalizedBody, err := normalize(email, options) headers, body, err := canonicalize(email, options)
if err != nil {
canonicalize(email, options) return nil, err
return nil, nil
} }
// hash body
var bodyHash string
var h1, h2 hash.Hash
var h3 crypto.Hash
signHash := strings.Split(options.Algo, "-")
if signHash[1] == "sha1" {
h1 = sha1.New()
h2 = sha1.New()
h3 = crypto.SHA1
} else {
h1 = sha256.New()
h2 = sha256.New()
h3 = crypto.SHA256
}
bodyHash = base64.StdEncoding.EncodeToString(h1.Sum(body))
// Get dkim header base
dkimHeader := NewDkimHeaderBySigOptions(options)
dHeader := dkimHeader.GetHeaderBase(bodyHash)
canonicalizations := strings.Split(options.Canonicalization, "/")
dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0])
if err != nil {
return nil, err
}
headers = append(headers, []byte(dHeaderCanonicalized)...)
// sign
h2.Write(headers)
sig, err := rsa.SignPKCS1v15(rand.Reader, privateKey, h3, h2.Sum(nil))
if err != nil {
return nil, err
}
sig64 := base64.StdEncoding.EncodeToString(sig)
// add to DKIM-Header
dHeader += ";" + FWS
subh := "b="
l := len(subh)
for _, c := range sig64 {
subh += string(c)
l++
if l >= MaxHeaderLineLength {
dHeader += subh + FWS
subh = ""
l = 0
}
}
dHeader += subh + CRLF
// Out
rawmail := []byte(dHeader)
t, err := ioutil.ReadAll(email)
if err != nil {
return nil, err
}
rawmail = append(rawmail, t...)
return bytes.NewReader(rawmail), nil
}
// canonicalize returns canonicalized version of header and body
func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body []byte, err error) { func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body []byte, err error) {
var email []byte var email []byte
body = []byte{} body = []byte{}
@@ -157,8 +241,6 @@ func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body
canonicalizations := strings.Split(options.Canonicalization, "/") canonicalizations := strings.Split(options.Canonicalization, "/")
// canonicalyze header // canonicalyze header
//var headersMap [][]byte
//headersMap := [][]byte{}
headersList := list.New() headersList := list.New()
currentHeader := []byte{} currentHeader := []byte{}
for _, line := range bytes.SplitAfter(parts[0], []byte{10}) { for _, line := range bytes.SplitAfter(parts[0], []byte{10}) {
@@ -170,7 +252,6 @@ func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body
} else { } else {
// New header, save current if exists // New header, save current if exists
if len(currentHeader) != 0 { if len(currentHeader) != 0 {
//headersMap = append(headersMap, currentHeader)
headersList.PushBack(string(currentHeader)) headersList.PushBack(string(currentHeader))
currentHeader = []byte{} currentHeader = []byte{}
@@ -201,51 +282,13 @@ func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body
} }
} }
if canonicalizations[0] == "simple" { //if canonicalizations[0] == "simple" {
// The "simple" header canonicalization algorithm does not change header
// fields in any way. Header fields MUST be presented to the signing or
// verification algorithm exactly as they are in the message being
// signed or verified. In particular, header field names MUST NOT be
// case folded and whitespace MUST NOT be changed.
for e := headersToKeepList.Front(); e != nil; e = e.Next() { for e := headersToKeepList.Front(); e != nil; e = e.Next() {
headers = append(headers, []byte(e.Value.(string))...) cHeader, err := canonicalizeHeader(e.Value.(string), canonicalizations[0])
} if err != nil {
} else { return headers, body, err
// The "relaxed" header canonicalization algorithm MUST apply the
// following steps in order:
// Convert all header field names (not the header field values) to
// lowercase. For example, convert "SUBJect: AbC" to "subject: AbC".
// Unfold all header field continuation lines as described in
// [RFC5322]; in particular, lines with terminators embedded in
// continued header field values (that is, CRLF sequences followed by
// WSP) MUST be interpreted without the CRLF. Implementations MUST
// NOT remove the CRLF at the end of the header field value.
// Convert all sequences of one or more WSP characters to a single SP
// character. WSP characters here include those before and after a
// line folding boundary.
// Delete all WSP characters at the end of each unfolded header field
// value.
// Delete any WSP characters remaining before and after the colon
// separating the header field name from the header field value. The
// colon separator MUST be retained.
for e := headersToKeepList.Front(); e != nil; e = e.Next() {
kv := strings.SplitN(e.Value.(string), ":", 2)
if len(kv) != 2 {
return []byte{}, []byte{}, ErrBadMailFormatHeaders
}
k := strings.ToLower(kv[0])
k = strings.TrimSpace(k)
v := strings.Replace(kv[1], "\n", "", -1)
v = strings.Replace(v, "\r", "", -1)
v = rxReduceWS.ReplaceAllString(v, " ")
v = strings.TrimSpace(v)
headers = append(headers, []byte(k+":"+v+CRLF)...)
} }
headers = append(headers, []byte(cHeader)...)
} }
// canonicalyze body // canonicalyze body
if canonicalizations[1] == "simple" { if canonicalizations[1] == "simple" {
@@ -290,3 +333,51 @@ func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body
println(string(body))*/ println(string(body))*/
return return
} }
// canonicalizeHeader returns canonicalized version of header
func canonicalizeHeader(header string, algo string) (string, error) {
rxReduceWS := regexp.MustCompile(`[ \t]+`)
if algo == "simple" {
// The "simple" header canonicalization algorithm does not change header
// fields in any way. Header fields MUST be presented to the signing or
// verification algorithm exactly as they are in the message being
// signed or verified. In particular, header field names MUST NOT be
// case folded and whitespace MUST NOT be changed.
return header, nil
} else if algo == "relaxed" {
// The "relaxed" header canonicalization algorithm MUST apply the
// following steps in order:
// Convert all header field names (not the header field values) to
// lowercase. For example, convert "SUBJect: AbC" to "subject: AbC".
// Unfold all header field continuation lines as described in
// [RFC5322]; in particular, lines with terminators embedded in
// continued header field values (that is, CRLF sequences followed by
// WSP) MUST be interpreted without the CRLF. Implementations MUST
// NOT remove the CRLF at the end of the header field value.
// Convert all sequences of one or more WSP characters to a single SP
// character. WSP characters here include those before and after a
// line folding boundary.
// Delete all WSP characters at the end of each unfolded header field
// value.
// Delete any WSP characters remaining before and after the colon
// separating the header field name from the header field value. The
// colon separator MUST be retained.
kv := strings.SplitN(header, ":", 2)
if len(kv) != 2 {
return header, ErrBadMailFormatHeaders
}
k := strings.ToLower(kv[0])
k = strings.TrimSpace(k)
v := strings.Replace(kv[1], "\n", "", -1)
v = strings.Replace(v, "\r", "", -1)
v = rxReduceWS.ReplaceAllString(v, " ")
v = strings.TrimSpace(v)
return k + ":" + v + CRLF, nil
}
return header, ErrSignBadCanonicalization
}

View File

@@ -1,6 +1,8 @@
package dkim package dkim
import ( import (
"fmt"
"strings"
"time" "time"
) )
@@ -180,3 +182,99 @@ type DkimHeader struct {
// tag z // tag z
CopiedHeaderFileds []string CopiedHeaderFileds []string
} }
// NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value
func NewDkimHeaderBySigOptions(options sigOptions) *DkimHeader {
h := new(DkimHeader)
h.Version = "1"
h.Algorithm = options.Algo
h.MessageCanonicalization = options.Canonicalization
h.Domain = options.Domain
h.Headers = options.Headers
h.Auid = options.Auid
h.BodyLength = options.BodyLength
h.QueryMethods = options.QueryMethods
h.Selector = options.Selector
if options.AddSignatureTimestamp {
h.SignatureTimestamp = time.Now()
}
if options.SignatureExpireIn.Seconds() > 0 {
h.SignatureExpiration = time.Now().Add(options.SignatureExpireIn)
}
h.CopiedHeaderFileds = options.CopiedHeaderFileds
return h
}
// GetHeaderBase return base header for signers
// Todo: some refactoring...
func (d *DkimHeader) GetHeaderBase(bodyHash string) string {
h := "DKIM-Signature: v=" + d.Version + "; a=" + d.Algorithm + "; q=" + strings.Join(d.QueryMethods, ":") + "; c=" + d.MessageCanonicalization + ";" + CRLF + TAB
subh := "s=" + d.Selector + ";"
if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength {
h += subh + FWS
subh = ""
}
subh += " d=" + d.Domain + ";"
// signature timestamp
if !d.SignatureTimestamp.IsZero() {
ts := d.SignatureTimestamp.Unix()
if len(subh)+14 > MaxHeaderLineLength {
h += subh + FWS
subh = ""
}
subh += " t=" + fmt.Sprintf("%d", ts) + ";"
}
if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength {
h += subh + FWS
subh = ""
}
// Expiration
if !d.SignatureExpiration.IsZero() {
ts := d.SignatureExpiration.Unix()
if len(subh)+14 > MaxHeaderLineLength {
h += subh + FWS
subh = ""
}
subh += " x=" + fmt.Sprintf("%d", ts) + ";"
}
// Headers
if len(subh)+len(d.Headers)+4 > MaxHeaderLineLength {
h += subh + FWS
subh = ""
}
subh += " h="
for _, header := range d.Headers {
if len(subh)+len(header)+1 > MaxHeaderLineLength {
h += subh + FWS
subh = ""
}
subh += header + ":"
}
subh = subh[:len(subh)-1] + ";"
// BodyHash
if len(subh)+5+len(bodyHash) > MaxHeaderLineLength {
h += subh + FWS
subh = ""
} else {
subh += " "
}
subh += "bh="
l := len(subh)
for _, c := range bodyHash {
subh += string(c)
l++
if l >= MaxHeaderLineLength {
h += subh + FWS
subh = ""
l = 0
}
}
h += subh
return h
}

View File

@@ -2,12 +2,15 @@ package dkim
import ( import (
"bytes" "bytes"
"fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"io/ioutil"
"testing" "testing"
) )
const ( const (
privKey = `MIICXQIBAAKBgQDNUXO+Qsl1tw+GjrqFajz0ERSEUs1FHSL/+udZRWn1Atw8gz0+ privKey = `-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDNUXO+Qsl1tw+GjrqFajz0ERSEUs1FHSL/+udZRWn1Atw8gz0+
tcGqhWChBDeU9gY5sKLEAZnX3FjC/T/IbqeiSM68kS5vLkzRI84eiJrm3+IieUqI tcGqhWChBDeU9gY5sKLEAZnX3FjC/T/IbqeiSM68kS5vLkzRI84eiJrm3+IieUqI
IicsO+WYxQs+JgVx5XhpPjX4SQjHtwEC2xKkWnEv+VPgO1JWdooURcSC6QIDAQAB IicsO+WYxQs+JgVx5XhpPjX4SQjHtwEC2xKkWnEv+VPgO1JWdooURcSC6QIDAQAB
AoGAM9exRgVPIS4L+Ynohu+AXJBDgfX2ZtEomUIdUGk6i+cg/RaWTFNQh2IOOBn8 AoGAM9exRgVPIS4L+Ynohu+AXJBDgfX2ZtEomUIdUGk6i+cg/RaWTFNQh2IOOBn8
@@ -19,7 +22,8 @@ uGhq0DPojmhsmUC8jUeLe79CllZNP3LU1wJBAIZcoCnI7g5Bcdr4nyxfJ4pkw4cQ
S4FT0XAZPR/YZrADo8/SWCWPdFTGSuaf17nL6vLD1zljK/skY5LwshrvUCMCQQDM S4FT0XAZPR/YZrADo8/SWCWPdFTGSuaf17nL6vLD1zljK/skY5LwshrvUCMCQQDM
MY7ehj6DVFHYlt2LFSyhInCZscTencgK24KfGF5t1JZlwt34YaMqjAMACmi/55Fc MY7ehj6DVFHYlt2LFSyhInCZscTencgK24KfGF5t1JZlwt34YaMqjAMACmi/55Fc
e7DIxW5nI/nDZrOY+EAjAkA3BHUx3PeXkXJnXjlh7nGZmk/v8tB5fiofAwfXNfL7 e7DIxW5nI/nDZrOY+EAjAkA3BHUx3PeXkXJnXjlh7nGZmk/v8tB5fiofAwfXNfL7
bz0ZrT2Caz995Dpjommh5aMpCJvUGsrYCG6/Pbha9NXl` bz0ZrT2Caz995Dpjommh5aMpCJvUGsrYCG6/Pbha9NXl
-----END RSA PRIVATE KEY-----`
pubKey = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNUXO+Qsl1tw+GjrqFajz0ERSE pubKey = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNUXO+Qsl1tw+GjrqFajz0ERSE
Us1FHSL/+udZRWn1Atw8gz0+tcGqhWChBDeU9gY5sKLEAZnX3FjC/T/IbqeiSM68 Us1FHSL/+udZRWn1Atw8gz0+tcGqhWChBDeU9gY5sKLEAZnX3FjC/T/IbqeiSM68
@@ -90,45 +94,55 @@ func Test_SignConfig(t *testing.T) {
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 = "toto" options.PrivateKey = privKey
_, err = Sign(emailReader, options) _, err = Sign(emailReader, options)
emailReader.Seek(0, 0)
// Domain // Domain
assert.EqualError(t, err, ErrSignDomainRequired.Error()) assert.EqualError(t, err, ErrSignDomainRequired.Error())
options.Domain = "toorop.fr" options.Domain = "toorop.fr"
_, err = Sign(emailReader, options) _, err = Sign(emailReader, options)
emailReader.Seek(0, 0)
// Selector // Selector
assert.Error(t, err, ErrSignSelectorRequired.Error()) assert.Error(t, err, ErrSignSelectorRequired.Error())
options.Selector = "default" options.Selector = "default"
_, err = Sign(emailReader, options) _, err = Sign(emailReader, options)
assert.NoError(t, err) assert.NoError(t, err)
emailReader.Seek(0, 0)
// Canonicalization // Canonicalization
options.Canonicalization = "simple/relaxed/simple" options.Canonicalization = "simple/relaxed/simple"
_, err = Sign(emailReader, options) _, err = Sign(emailReader, options)
assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) assert.EqualError(t, err, ErrSignBadCanonicalization.Error())
emailReader.Seek(0, 0)
options.Canonicalization = "simple/relax" options.Canonicalization = "simple/relax"
_, err = Sign(emailReader, options) _, err = Sign(emailReader, options)
emailReader.Seek(0, 0)
assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) assert.EqualError(t, err, ErrSignBadCanonicalization.Error())
emailReader.Seek(0, 0)
options.Canonicalization = "relaxed" options.Canonicalization = "relaxed"
_, err = Sign(emailReader, options) _, err = Sign(emailReader, options)
assert.NoError(t, err) assert.NoError(t, err)
emailReader.Seek(0, 0)
options.Canonicalization = "SiMple/relAxed" options.Canonicalization = "SiMple/relAxed"
_, err = Sign(emailReader, options) _, err = Sign(emailReader, options)
assert.NoError(t, err) assert.NoError(t, err)
emailReader.Seek(0, 0)
// header // header
options.Headers = []string{"toto"} options.Headers = []string{"toto"}
_, err = Sign(emailReader, options) _, err = Sign(emailReader, options)
assert.EqualError(t, err, ErrSignHeaderShouldContainsFrom.Error()) assert.EqualError(t, err, ErrSignHeaderShouldContainsFrom.Error())
emailReader.Seek(0, 0)
options.Headers = []string{"To", "From"} options.Headers = []string{"To", "From"}
_, err = Sign(emailReader, options) _, err = Sign(emailReader, options)
assert.NoError(t, err) assert.NoError(t, err)
emailReader.Seek(0, 0)
} }
func Test_canonicalize(t *testing.T) { func Test_canonicalize(t *testing.T) {
@@ -155,9 +169,14 @@ func Test_Sign(t *testing.T) {
emailReader := bytes.NewReader([]byte(email)) emailReader := bytes.NewReader([]byte(email))
options := NewSigOptions() options := NewSigOptions()
options.PrivateKey = privKey options.PrivateKey = privKey
options.Canonicalization = "simple/relaxed" options.Canonicalization = "relaxed/relaxed"
options.Domain = domain options.Domain = domain
options.Selector = selector options.Selector = selector
options.AddSignatureTimestamp = true
options.SignatureExpireIn = 3600
options.Headers = []string{"from", "date", "mime-version", "received", "received", "In-Reply-To"}
emailReader, err := Sign(emailReader, options) emailReader, err := Sign(emailReader, options)
assert.NoError(t, err) assert.NoError(t, err)
raw, _ := ioutil.ReadAll(emailReader)
fmt.Println(string(raw))
} }