query.go raw
1 package dkim
2
3 import (
4 "crypto"
5 "crypto/rsa"
6 "crypto/x509"
7 "encoding/base64"
8 "errors"
9 "fmt"
10 "net"
11 "strings"
12
13 "golang.org/x/crypto/ed25519"
14 )
15
16 type verifier interface {
17 Public() crypto.PublicKey
18 Verify(hash crypto.Hash, hashed []byte, sig []byte) error
19 }
20
21 type rsaVerifier struct {
22 *rsa.PublicKey
23 }
24
25 func (v rsaVerifier) Public() crypto.PublicKey {
26 return v.PublicKey
27 }
28
29 func (v rsaVerifier) Verify(hash crypto.Hash, hashed, sig []byte) error {
30 return rsa.VerifyPKCS1v15(v.PublicKey, hash, hashed, sig)
31 }
32
33 type ed25519Verifier struct {
34 ed25519.PublicKey
35 }
36
37 func (v ed25519Verifier) Public() crypto.PublicKey {
38 return v.PublicKey
39 }
40
41 func (v ed25519Verifier) Verify(hash crypto.Hash, hashed, sig []byte) error {
42 if !ed25519.Verify(v.PublicKey, hashed, sig) {
43 return errors.New("dkim: invalid Ed25519 signature")
44 }
45 return nil
46 }
47
48 type queryResult struct {
49 Verifier verifier
50 KeyAlgo string
51 HashAlgos []string
52 Notes string
53 Services []string
54 Flags []string
55 }
56
57 // QueryMethod is a DKIM query method.
58 type QueryMethod string
59
60 const (
61 // DNS TXT resource record (RR) lookup algorithm
62 QueryMethodDNSTXT QueryMethod = "dns/txt"
63 )
64
65 type txtLookupFunc func(domain string) ([]string, error)
66 type queryFunc func(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error)
67
68 var queryMethods = map[QueryMethod]queryFunc{
69 QueryMethodDNSTXT: queryDNSTXT,
70 }
71
72 func queryDNSTXT(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error) {
73 if txtLookup == nil {
74 txtLookup = net.LookupTXT
75 }
76
77 txts, err := txtLookup(selector + "._domainkey." + domain)
78 if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
79 return nil, tempFailError("key unavailable: " + err.Error())
80 } else if err != nil {
81 return nil, permFailError("no key for signature: " + err.Error())
82 }
83
84 // net.LookupTXT will concatenate strings contained in a single TXT record.
85 // In other words, net.LookupTXT returns one entry per TXT record, even if
86 // a record contains multiple strings.
87 //
88 // RFC 6376 section 3.6.2.2 says multiple TXT records lead to undefined
89 // behavior, so reject that.
90 switch len(txts) {
91 case 0:
92 return nil, permFailError("no valid key found")
93 case 1:
94 return parsePublicKey(txts[0])
95 default:
96 return nil, permFailError("multiple TXT records found for key")
97 }
98 }
99
100 func parsePublicKey(s string) (*queryResult, error) {
101 params, err := parseHeaderParams(s)
102 if err != nil {
103 return nil, permFailError("key record error: " + err.Error())
104 }
105
106 res := new(queryResult)
107
108 if v, ok := params["v"]; ok && v != "DKIM1" {
109 return nil, permFailError("incompatible public key version")
110 }
111
112 p, ok := params["p"]
113 if !ok {
114 return nil, permFailError("key syntax error: missing public key data")
115 }
116 if p == "" {
117 return nil, permFailError("key revoked")
118 }
119 p = strings.ReplaceAll(p, " ", "")
120 b, err := base64.StdEncoding.DecodeString(p)
121 if err != nil {
122 return nil, permFailError("key syntax error: " + err.Error())
123 }
124 switch params["k"] {
125 case "rsa", "":
126 pub, err := x509.ParsePKIXPublicKey(b)
127 if err != nil {
128 // RFC 6376 is inconsistent about whether RSA public keys should
129 // be formatted as RSAPublicKey or SubjectPublicKeyInfo.
130 // Erratum 3017 (https://www.rfc-editor.org/errata/eid3017) proposes
131 // allowing both.
132 pub, err = x509.ParsePKCS1PublicKey(b)
133 if err != nil {
134 return nil, permFailError("key syntax error: " + err.Error())
135 }
136 }
137 rsaPub, ok := pub.(*rsa.PublicKey)
138 if !ok {
139 return nil, permFailError("key syntax error: not an RSA public key")
140 }
141 // RFC 8301 section 3.2: verifiers MUST NOT consider signatures using
142 // RSA keys of less than 1024 bits as valid signatures.
143 if rsaPub.Size()*8 < 1024 {
144 return nil, permFailError(fmt.Sprintf("key is too short: want 1024 bits, has %v bits", rsaPub.Size()*8))
145 }
146 res.Verifier = rsaVerifier{rsaPub}
147 res.KeyAlgo = "rsa"
148 case "ed25519":
149 if len(b) != ed25519.PublicKeySize {
150 return nil, permFailError(fmt.Sprintf("invalid Ed25519 public key size: %v bytes", len(b)))
151 }
152 ed25519Pub := ed25519.PublicKey(b)
153 res.Verifier = ed25519Verifier{ed25519Pub}
154 res.KeyAlgo = "ed25519"
155 default:
156 return nil, permFailError("unsupported key algorithm")
157 }
158
159 if hashesStr, ok := params["h"]; ok {
160 res.HashAlgos = parseTagList(hashesStr)
161 }
162 if notes, ok := params["n"]; ok {
163 res.Notes = notes
164 }
165 if servicesStr, ok := params["s"]; ok {
166 services := parseTagList(servicesStr)
167
168 hasWildcard := false
169 for _, s := range services {
170 if s == "*" {
171 hasWildcard = true
172 break
173 }
174 }
175 if !hasWildcard {
176 res.Services = services
177 }
178 }
179 if flagsStr, ok := params["t"]; ok {
180 res.Flags = parseTagList(flagsStr)
181 }
182
183 return res, nil
184 }
185