1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
|
package dkim
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"mime/quotedprintable"
"net"
"strings"
)
// PubKeyRep represents a parsed version of public key record
type PubKeyRep struct {
Version string
HashAlgo []string
KeyType string
Note string
PubKey rsa.PublicKey
ServiceType []string
FlagTesting bool // flag y
FlagIMustBeD bool // flag i
}
// DNSOptions holds settings for looking up DNS records
type DNSOptions struct {
netLookupTXT func(name string) ([]string, error)
}
// DNSOpt represents an optional setting for looking up DNS records
type DNSOpt interface {
apply(*DNSOptions)
}
type dnsOpt func(*DNSOptions)
func (opt dnsOpt) apply(dnsOpts *DNSOptions) {
opt(dnsOpts)
}
// DNSOptLookupTXT sets the function to use to lookup TXT records.
//
// This should probably only be used in tests.
func DNSOptLookupTXT(netLookupTXT func(name string) ([]string, error)) DNSOpt {
return dnsOpt(func(opts *DNSOptions) {
opts.netLookupTXT = netLookupTXT
})
}
// NewPubKeyRespFromDNS retrieves the TXT record from DNS based on the specified domain and selector
// and parses it.
func NewPubKeyRespFromDNS(selector, domain string, opts ...DNSOpt) (*PubKeyRep, verifyOutput, error) {
dnsOpts := DNSOptions{}
for _, opt := range opts {
opt.apply(&dnsOpts)
}
if dnsOpts.netLookupTXT == nil {
dnsOpts.netLookupTXT = net.LookupTXT
}
txt, err := dnsOpts.netLookupTXT(selector + "._domainkey." + domain)
if err != nil {
if strings.HasSuffix(err.Error(), "no such host") {
return nil, PERMFAIL, ErrVerifyNoKeyForSignature
}
return nil, TEMPFAIL, ErrVerifyKeyUnavailable
}
// empty record
if len(txt) == 0 {
return nil, PERMFAIL, ErrVerifyNoKeyForSignature
}
// parsing, we keep the first record
// TODO: if there is multiple record
return NewPubKeyResp(txt[0])
}
// NewPubKeyResp parses DKIM record (usually from DNS)
func NewPubKeyResp(dkimRecord string) (*PubKeyRep, verifyOutput, error) {
pkr := new(PubKeyRep)
pkr.Version = "DKIM1"
pkr.HashAlgo = []string{"sha1", "sha256"}
pkr.KeyType = "rsa"
pkr.FlagTesting = false
pkr.FlagIMustBeD = false
p := strings.Split(dkimRecord, ";")
for i, data := range p {
keyVal := strings.SplitN(data, "=", 2)
val := ""
if len(keyVal) > 1 {
val = strings.TrimSpace(keyVal[1])
}
switch strings.ToLower(strings.TrimSpace(keyVal[0])) {
case "v":
// RFC: is this tag is specified it MUST be the first in the record
if i != 0 {
return nil, PERMFAIL, ErrVerifyTagVMustBeTheFirst
}
pkr.Version = val
if pkr.Version != "DKIM1" {
return nil, PERMFAIL, ErrVerifyVersionMusBeDkim1
}
case "h":
p := strings.Split(strings.ToLower(val), ":")
pkr.HashAlgo = []string{}
for _, h := range p {
h = strings.TrimSpace(h)
if h == "sha1" || h == "sha256" {
pkr.HashAlgo = append(pkr.HashAlgo, h)
}
}
// if empty switch back to default
if len(pkr.HashAlgo) == 0 {
pkr.HashAlgo = []string{"sha1", "sha256"}
}
case "k":
if strings.ToLower(val) != "rsa" {
return nil, PERMFAIL, ErrVerifyBadKeyType
}
case "n":
qp, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(val)))
if err == nil {
val = string(qp)
}
pkr.Note = val
case "p":
rawkey := val
if rawkey == "" {
return nil, PERMFAIL, ErrVerifyRevokedKey
}
un64, err := base64.StdEncoding.DecodeString(rawkey)
if err != nil {
return nil, PERMFAIL, ErrVerifyBadKey
}
pk, err := x509.ParsePKIXPublicKey(un64)
if pk, ok := pk.(*rsa.PublicKey); ok {
pkr.PubKey = *pk
}
case "s":
t := strings.Split(strings.ToLower(val), ":")
for _, tt := range t {
tt = strings.TrimSpace(tt)
switch tt {
case "*":
pkr.ServiceType = append(pkr.ServiceType, "all")
case "email":
pkr.ServiceType = append(pkr.ServiceType, tt)
}
}
case "t":
flags := strings.Split(strings.ToLower(val), ":")
for _, flag := range flags {
flag = strings.TrimSpace(flag)
switch flag {
case "y":
pkr.FlagTesting = true
case "s":
pkr.FlagIMustBeD = true
}
}
}
}
// if no pubkey
if pkr.PubKey == (rsa.PublicKey{}) {
return nil, PERMFAIL, ErrVerifyNoKey
}
// No service type
if len(pkr.ServiceType) == 0 {
pkr.ServiceType = []string{"all"}
}
return pkr, SUCCESS, nil
}
|