From 76ada32b776fdde0d1305ea894afb1e153ff5978 Mon Sep 17 00:00:00 2001 From: "Dolf Schimmel (Freeaqingme)" Date: Tue, 19 Jul 2016 20:53:39 +0200 Subject: [PATCH] Allow to use custom signer, only get header value, get pub key from DNS --- dkim.go | 123 ++++++++++++++++++++++++++++++++++----------------- dkim_test.go | 1 - pubKeyRep.go | 10 +++-- 3 files changed, 88 insertions(+), 46 deletions(-) diff --git a/dkim.go b/dkim.go index 9638e95..e8e589a 100644 --- a/dkim.go +++ b/dkim.go @@ -12,6 +12,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + "errors" "hash" "regexp" "strings" @@ -83,6 +84,22 @@ type SigOptions struct { CopiedHeaderFields []string } +type Signer interface { + Sign(message []byte, algo string) (string, error) +} + +type DefaultSigner struct { + PrivateKey *rsa.PrivateKey +} + +func (s *DefaultSigner) Sign(message []byte, algo string) (string, error) { + if s.PrivateKey == nil { + return "", errors.New("PrivateKey must be set before signing") + } + + return getSignature(message, s.PrivateKey, algo) +} + // NewSigOptions returns new sigoption with some defaults value func NewSigOptions() SigOptions { return SigOptions{ @@ -97,23 +114,27 @@ func NewSigOptions() SigOptions { } } -// Sign signs an email -func Sign(email *[]byte, options SigOptions) error { - var privateKey *rsa.PrivateKey - - // PrivateKey +func getPrivateKeyFromSigOptions(options SigOptions) (*rsa.PrivateKey, error) { if len(options.PrivateKey) == 0 { - return ErrSignPrivateKeyRequired + return nil, ErrSignPrivateKeyRequired } d, _ := pem.Decode(options.PrivateKey) if d == nil { - return ErrCandNotParsePrivateKey + return nil, ErrCandNotParsePrivateKey } key, err := x509.ParsePKCS1PrivateKey(d.Bytes) if err != nil { - return ErrCandNotParsePrivateKey + return nil, ErrCandNotParsePrivateKey + } + return key, nil +} + +// Sign signs an email +func Sign(email *[]byte, options SigOptions) error { + privateKey, err := getPrivateKeyFromSigOptions(options) + if err != nil { + return err } - privateKey = key // Domain required if options.Domain == "" { @@ -150,36 +171,32 @@ func Sign(email *[]byte, options SigOptions) error { return ErrSignHeaderShouldContainsFrom } - // Normalize - headers, body, err := canonicalize(email, options.Canonicalization, options.Headers) + signer := &DefaultSigner{ + PrivateKey: privateKey, + } + + dHeader, err := GetDkimHeader(*email, signer, &options) if err != nil { return err } + dHeader = "DKIM-Signature: " + dHeader + CRLF + *email = append([]byte(dHeader), *email...) + return nil +} + +func GetDkimHeader(email []byte, signer Signer, options *SigOptions) (string, error) { + headers, dHeader, err := getHashString(email, options) + if err != nil { + return "", err + } + signHash := strings.Split(options.Algo, "-") - - // hash body - bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength) + sig, err := signer.Sign(headers, signHash[1]) if err != nil { - return err + return "", err } - // Get dkim header base - dkimHeader := newDkimHeaderBySigOptions(options) - dHeader := dkimHeader.getHeaderBaseForSigning(bodyHash) - - canonicalizations := strings.Split(options.Canonicalization, "/") - dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) - if err != nil { - return err - } - headers = append(headers, []byte(dHeaderCanonicalized)...) - headers = bytes.TrimRight(headers, " \r\n") - - // sign - sig, err := getSignature(&headers, privateKey, signHash[1]) - - // add to DKIM-Header subh := "" l := len(subh) for _, c := range sig { @@ -191,9 +208,36 @@ func Sign(email *[]byte, options SigOptions) error { l = 0 } } - dHeader += subh + CRLF - *email = append([]byte(dHeader), *email...) - return nil + dHeader += subh + + return dHeader[len("DKIM-Signature: "):], nil +} + +func getHashString(email []byte, options *SigOptions) (headers []byte, dheader string, err error) { + headers, body, err := canonicalize(&email, options.Canonicalization, options.Headers) + if err != nil { + return []byte{}, "", err + } + + signHash := strings.Split(options.Algo, "-") + + bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength) + if err != nil { + return []byte{}, "", err + } + + dkimHeader := newDkimHeaderBySigOptions(*options) + dHeader := dkimHeader.getHeaderBaseForSigning(bodyHash) + + canonicalizations := strings.Split(options.Canonicalization, "/") + dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) + if err != nil { + return []byte{}, "", err + } + headers = append(headers, []byte(dHeaderCanonicalized)...) + headers = bytes.TrimRight(headers, " \r\n") + + return headers, dHeader, nil } // Verify verifies an email an return @@ -211,12 +255,11 @@ func Verify(email *[]byte) (verifyOutput, error) { } // we do not set query method because if it's others, validation failed earlier - pubKey, verifyOutputOnError, err := newPubKeyFromDnsTxt(dkimHeader.Selector, dkimHeader.Domain) + pubKeys, verifyOutputOnError, err := PubKeyFromDns(dkimHeader.Selector, dkimHeader.Domain) if err != nil { - // fix https://github.com/toorop/go-dkim/issues/1 - //return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting) return verifyOutputOnError, err } + pubKey := pubKeys[0] // Normalize headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) @@ -239,10 +282,8 @@ func Verify(email *[]byte) (verifyOutput, error) { // expired ? if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() { return getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting) - } - //println("|" + string(body) + "|") // get body hash bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength) if err != nil { @@ -433,7 +474,7 @@ func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) { } // getSignature return signature of toSign using key -func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, error) { +func getSignature(toSign []byte, key *rsa.PrivateKey, algo string) (string, error) { var h1 hash.Hash var h2 crypto.Hash switch algo { @@ -450,7 +491,7 @@ func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, err } // sign - h1.Write(*toSign) + h1.Write(toSign) sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil)) if err != nil { return "", err diff --git a/dkim_test.go b/dkim_test.go index d851789..f4b1a52 100644 --- a/dkim_test.go +++ b/dkim_test.go @@ -1,7 +1,6 @@ package dkim import ( - //"fmt" "testing" "github.com/stretchr/testify/assert" diff --git a/pubKeyRep.go b/pubKeyRep.go index 84f3989..f58f7e6 100644 --- a/pubKeyRep.go +++ b/pubKeyRep.go @@ -9,7 +9,7 @@ import ( ) // pubKeyRep represents a parsed version of public key record -type pubKeyRep struct { +type PubKey struct { Version string HashAlgo []string KeyType string @@ -18,9 +18,10 @@ type pubKeyRep struct { ServiceType []string FlagTesting bool // flag y FlagIMustBeD bool // flag i + Selector string } -func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, error) { +func PubKeyFromDns(selector, domain string) ([]*PubKey, verifyOutput, error) { txt, err := net.LookupTXT(selector + "._domainkey." + domain) if err != nil { if strings.HasSuffix(err.Error(), "no such host") { @@ -35,13 +36,14 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err return nil, PERMFAIL, ErrVerifyNoKeyForSignature } - pkr := new(pubKeyRep) + pkr := new(PubKey) pkr.Version = "DKIM1" pkr.HashAlgo = []string{"sha1", "sha256"} pkr.KeyType = "rsa" pkr.ServiceType = []string{"all"} pkr.FlagTesting = false pkr.FlagIMustBeD = false + pkr.Selector = selector // parsing, we keep the first record // TODO: if there is multiple record @@ -123,5 +125,5 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err return nil, PERMFAIL, ErrVerifyNoKey } - return pkr, SUCCESS, nil + return []*PubKey{pkr}, SUCCESS, nil }