Allow to use custom signer, only get header value, get pub key from DNS

This commit is contained in:
Dolf Schimmel (Freeaqingme)
2016-07-19 20:53:39 +02:00
parent 5359189bbe
commit 76ada32b77
3 changed files with 88 additions and 46 deletions

123
dkim.go
View File

@@ -12,6 +12,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/pem" "encoding/pem"
"errors"
"hash" "hash"
"regexp" "regexp"
"strings" "strings"
@@ -83,6 +84,22 @@ type SigOptions struct {
CopiedHeaderFields []string 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 // NewSigOptions returns new sigoption with some defaults value
func NewSigOptions() SigOptions { func NewSigOptions() SigOptions {
return SigOptions{ return SigOptions{
@@ -97,23 +114,27 @@ func NewSigOptions() SigOptions {
} }
} }
// Sign signs an email func getPrivateKeyFromSigOptions(options SigOptions) (*rsa.PrivateKey, error) {
func Sign(email *[]byte, options SigOptions) error {
var privateKey *rsa.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)
if d == nil { if d == nil {
return ErrCandNotParsePrivateKey return nil, ErrCandNotParsePrivateKey
} }
key, err := x509.ParsePKCS1PrivateKey(d.Bytes) key, err := x509.ParsePKCS1PrivateKey(d.Bytes)
if err != nil { 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 // Domain required
if options.Domain == "" { if options.Domain == "" {
@@ -150,36 +171,32 @@ func Sign(email *[]byte, options SigOptions) error {
return ErrSignHeaderShouldContainsFrom return ErrSignHeaderShouldContainsFrom
} }
// Normalize signer := &DefaultSigner{
headers, body, err := canonicalize(email, options.Canonicalization, options.Headers) PrivateKey: privateKey,
}
dHeader, err := GetDkimHeader(*email, signer, &options)
if err != nil { if err != nil {
return err 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, "-") signHash := strings.Split(options.Algo, "-")
sig, err := signer.Sign(headers, signHash[1])
// hash body
bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength)
if err != nil { 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 := "" subh := ""
l := len(subh) l := len(subh)
for _, c := range sig { for _, c := range sig {
@@ -191,9 +208,36 @@ func Sign(email *[]byte, options SigOptions) error {
l = 0 l = 0
} }
} }
dHeader += subh + CRLF dHeader += subh
*email = append([]byte(dHeader), *email...)
return nil 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 // 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 // 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 { if err != nil {
// fix https://github.com/toorop/go-dkim/issues/1
//return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting)
return verifyOutputOnError, err return verifyOutputOnError, err
} }
pubKey := pubKeys[0]
// Normalize // Normalize
headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers)
@@ -239,10 +282,8 @@ func Verify(email *[]byte) (verifyOutput, error) {
// 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 getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting)
} }
//println("|" + string(body) + "|")
// get body hash // get body hash
bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength) bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength)
if err != nil { if err != nil {
@@ -433,7 +474,7 @@ func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) {
} }
// getSignature return signature of toSign using key // 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 h1 hash.Hash
var h2 crypto.Hash var h2 crypto.Hash
switch algo { switch algo {
@@ -450,7 +491,7 @@ func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, err
} }
// sign // sign
h1.Write(*toSign) h1.Write(toSign)
sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil)) sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil))
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -1,7 +1,6 @@
package dkim package dkim
import ( import (
//"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"

View File

@@ -9,7 +9,7 @@ import (
) )
// pubKeyRep represents a parsed version of public key record // pubKeyRep represents a parsed version of public key record
type pubKeyRep struct { type PubKey struct {
Version string Version string
HashAlgo []string HashAlgo []string
KeyType string KeyType string
@@ -18,9 +18,10 @@ type pubKeyRep struct {
ServiceType []string ServiceType []string
FlagTesting bool // flag y FlagTesting bool // flag y
FlagIMustBeD bool // flag i 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) 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") {
@@ -35,13 +36,14 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err
return nil, PERMFAIL, ErrVerifyNoKeyForSignature return nil, PERMFAIL, ErrVerifyNoKeyForSignature
} }
pkr := new(pubKeyRep) pkr := new(PubKey)
pkr.Version = "DKIM1" pkr.Version = "DKIM1"
pkr.HashAlgo = []string{"sha1", "sha256"} pkr.HashAlgo = []string{"sha1", "sha256"}
pkr.KeyType = "rsa" pkr.KeyType = "rsa"
pkr.ServiceType = []string{"all"} pkr.ServiceType = []string{"all"}
pkr.FlagTesting = false pkr.FlagTesting = false
pkr.FlagIMustBeD = false pkr.FlagIMustBeD = false
pkr.Selector = selector
// parsing, we keep the first record // parsing, we keep the first record
// TODO: if there is multiple record // TODO: if there is multiple record
@@ -123,5 +125,5 @@ func newPubKeyFromDnsTxt(selector, domain string) (*pubKeyRep, verifyOutput, err
return nil, PERMFAIL, ErrVerifyNoKey return nil, PERMFAIL, ErrVerifyNoKey
} }
return pkr, SUCCESS, nil return []*PubKey{pkr}, SUCCESS, nil
} }