Allow to use custom signer, only get header value, get pub key from DNS
This commit is contained in:
123
dkim.go
123
dkim.go
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dkim
|
package dkim
|
||||||
|
|
||||||
import (
|
import (
|
||||||
//"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|||||||
10
pubKeyRep.go
10
pubKeyRep.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user