1package dkim
2
3import (
4 "crypto/ed25519"
5 "crypto/rsa"
6 "crypto/x509"
7 "encoding/base64"
8 "errors"
9 "fmt"
10 "strings"
11)
12
13// Record is a DKIM DNS record, served on <selector>._domainkey.<domain> for a
14// given selector and domain (s= and d= in the DKIM-Signature).
15//
16// The record is a semicolon-separated list of "="-separated field value pairs.
17// Strings should be compared case-insensitively, e.g. k=ed25519 is equivalent to k=ED25519.
18//
19// Example:
20//
21// v=DKIM1;h=sha256;k=ed25519;p=ln5zd/JEX4Jy60WAhUOv33IYm2YZMyTQAdr9stML504=
22type Record struct {
23 Version string // Version, fixed "DKIM1" (case sensitive). Field "v".
24 Hashes []string // Acceptable hash algorithms, e.g. "sha1", "sha256". Optional, defaults to all algorithms. Field "h".
25 Key string // Key type, "rsa" or "ed25519". Optional, default "rsa". Field "k".
26 Notes string // Debug notes. Field "n".
27 Pubkey []byte // Public key, as base64 in record. If empty, the key has been revoked. Field "p".
28 Services []string // Service types. Optional, default "*" for all services. Other values: "email". Field "s".
29 Flags []string // Flags, colon-separated. Optional, default is no flags. Other values: "y" for testing DKIM, "s" for "i=" must have same domain as "d" in signatures. Field "t".
30
31 PublicKey any `json:"-"` // Parsed form of public key, an *rsa.PublicKey or ed25519.PublicKey.
32}
33
34// ../rfc/6376:1438
35
36// ServiceAllowed returns whether service s is allowed by this key.
37//
38// The optional field "s" can specify purposes for which the key can be used. If
39// value was specified, both "*" and "email" are enough for use with DKIM.
40func (r *Record) ServiceAllowed(s string) bool {
41 if len(r.Services) == 0 {
42 return true
43 }
44 for _, ss := range r.Services {
45 if ss == "*" || strings.EqualFold(s, ss) {
46 return true
47 }
48 }
49 return false
50}
51
52// Record returns a DNS TXT record that should be served at
53// <selector>._domainkey.<domain>.
54//
55// Only values that are not the default values are included.
56func (r *Record) Record() (string, error) {
57 var l []string
58 add := func(s string) {
59 l = append(l, s)
60 }
61
62 if r.Version != "DKIM1" {
63 return "", fmt.Errorf("bad version, must be \"DKIM1\"")
64 }
65 add("v=DKIM1")
66 if len(r.Hashes) > 0 {
67 add("h=" + strings.Join(r.Hashes, ":"))
68 }
69 if r.Key != "" && !strings.EqualFold(r.Key, "rsa") {
70 add("k=" + r.Key)
71 }
72 if r.Notes != "" {
73 add("n=" + qpSection(r.Notes))
74 }
75 if len(r.Services) > 0 && (len(r.Services) != 1 || r.Services[0] != "*") {
76 add("s=" + strings.Join(r.Services, ":"))
77 }
78 if len(r.Flags) > 0 {
79 add("t=" + strings.Join(r.Flags, ":"))
80 }
81 // A missing public key is valid, it means the key has been revoked. ../rfc/6376:1501
82 pk := r.Pubkey
83 if len(pk) == 0 && r.PublicKey != nil {
84 switch k := r.PublicKey.(type) {
85 case *rsa.PublicKey:
86 var err error
87 pk, err = x509.MarshalPKIXPublicKey(k)
88 if err != nil {
89 return "", fmt.Errorf("marshal rsa public key: %v", err)
90 }
91 case ed25519.PublicKey:
92 pk = []byte(k)
93 default:
94 return "", fmt.Errorf("unknown public key type %T", r.PublicKey)
95 }
96 }
97 add("p=" + base64.StdEncoding.EncodeToString(pk))
98 return strings.Join(l, ";"), nil
99}
100
101func qpSection(s string) string {
102 const hex = "0123456789ABCDEF"
103
104 // ../rfc/2045:1260
105 var r string
106 for i, b := range []byte(s) {
107 if i > 0 && (b == ' ' || b == '\t') || b > ' ' && b < 0x7f && b != '=' {
108 r += string(rune(b))
109 } else {
110 r += "=" + string(hex[b>>4]) + string(hex[(b>>0)&0xf])
111 }
112 }
113 return r
114}
115
116var (
117 errRecordDuplicateTag = errors.New("duplicate tag")
118 errRecordMissingField = errors.New("missing field")
119 errRecordBadPublicKey = errors.New("bad public key")
120 errRecordUnknownAlgorithm = errors.New("unknown algorithm")
121 errRecordVersionFirst = errors.New("first field must be version")
122)
123
124// ParseRecord parses a DKIM DNS TXT record.
125//
126// If the record is a dkim record, but an error occurred, isdkim will be true and
127// err will be the error. Such errors must be treated differently from parse errors
128// where the record does not appear to be DKIM, which can happen with misconfigured
129// DNS (e.g. wildcard records).
130func ParseRecord(s string) (record *Record, isdkim bool, err error) {
131 defer func() {
132 x := recover()
133 if x == nil {
134 return
135 }
136 if xerr, ok := x.(error); ok {
137 record = nil
138 err = xerr
139 return
140 }
141 panic(x)
142 }()
143
144 xerrorf := func(format string, args ...any) {
145 panic(fmt.Errorf(format, args...))
146 }
147
148 record = &Record{
149 Version: "DKIM1",
150 Key: "rsa",
151 Services: []string{"*"},
152 }
153
154 p := parser{s: s, drop: true}
155 seen := map[string]struct{}{}
156 // ../rfc/6376:655
157 // ../rfc/6376:656 ../rfc/6376-eid5070
158 // ../rfc/6376:658 ../rfc/6376-eid5070
159 // ../rfc/6376:1438
160 for {
161 p.fws()
162 k := p.xtagName()
163 p.fws()
164 p.xtake("=")
165 p.fws()
166 // Keys are case-sensitive: ../rfc/6376:679
167 if _, ok := seen[k]; ok {
168 // Duplicates not allowed: ../rfc/6376:683
169 xerrorf("%w: %q", errRecordDuplicateTag, k)
170 break
171 }
172 seen[k] = struct{}{}
173 // Version must be the first.
174 switch k {
175 case "v":
176 // ../rfc/6376:1443
177 v := p.xtake("DKIM1")
178 // Version being set is a signal this appears to be a valid record. We must not
179 // treat e.g. DKIM1.1 as valid, so we explicitly check there is no more data before
180 // we decide this record is DKIM.
181 p.fws()
182 if !p.empty() {
183 p.xtake(";")
184 }
185 record.Version = v
186 if len(seen) != 1 {
187 // If version is present, it must be the first.
188 xerrorf("%w", errRecordVersionFirst)
189 }
190 isdkim = true
191 if p.empty() {
192 break
193 }
194 continue
195
196 case "h":
197 // ../rfc/6376:1463
198 record.Hashes = []string{p.xhyphenatedWord()}
199 for p.peekfws(":") {
200 p.fws()
201 p.xtake(":")
202 p.fws()
203 record.Hashes = append(record.Hashes, p.xhyphenatedWord())
204 }
205 case "k":
206 // ../rfc/6376:1478
207 record.Key = p.xhyphenatedWord()
208 case "n":
209 // ../rfc/6376:1491
210 record.Notes = p.xqpSection()
211 case "p":
212 // ../rfc/6376:1501
213 record.Pubkey = p.xbase64()
214 case "s":
215 // ../rfc/6376:1533
216 record.Services = []string{p.xhyphenatedWord()}
217 for p.peekfws(":") {
218 p.fws()
219 p.xtake(":")
220 p.fws()
221 record.Services = append(record.Services, p.xhyphenatedWord())
222 }
223 case "t":
224 // ../rfc/6376:1554
225 record.Flags = []string{p.xhyphenatedWord()}
226 for p.peekfws(":") {
227 p.fws()
228 p.xtake(":")
229 p.fws()
230 record.Flags = append(record.Flags, p.xhyphenatedWord())
231 }
232 default:
233 // We must ignore unknown fields. ../rfc/6376:692 ../rfc/6376:1439
234 for !p.empty() && !p.hasPrefix(";") {
235 p.xchar()
236 }
237 }
238
239 isdkim = true
240 p.fws()
241 if p.empty() {
242 break
243 }
244 p.xtake(";")
245 if p.empty() {
246 break
247 }
248 }
249
250 if _, ok := seen["p"]; !ok {
251 xerrorf("%w: public key", errRecordMissingField)
252 }
253
254 switch strings.ToLower(record.Key) {
255 case "", "rsa":
256 if len(record.Pubkey) == 0 {
257 // Revoked key, nothing to do.
258 } else if pk, err := x509.ParsePKIXPublicKey(record.Pubkey); err != nil {
259 xerrorf("%w: %s", errRecordBadPublicKey, err)
260 } else if _, ok := pk.(*rsa.PublicKey); !ok {
261 xerrorf("%w: got %T, need an RSA key", errRecordBadPublicKey, record.PublicKey)
262 } else {
263 record.PublicKey = pk
264 }
265 case "ed25519":
266 if len(record.Pubkey) == 0 {
267 // Revoked key, nothing to do.
268 } else if len(record.Pubkey) != ed25519.PublicKeySize {
269 xerrorf("%w: got %d bytes, need %d", errRecordBadPublicKey, len(record.Pubkey), ed25519.PublicKeySize)
270 } else {
271 record.PublicKey = ed25519.PublicKey(record.Pubkey)
272 }
273 default:
274 xerrorf("%w: %q", errRecordUnknownAlgorithm, record.Key)
275 }
276
277 return record, true, nil
278}
279