1// Package dkim (DomainKeys Identified Mail signatures, RFC 6376) signs and
2// verifies DKIM signatures.
3//
4// Signatures are added to email messages in DKIM-Signature headers. By signing a
5// message, a domain takes responsibility for the message. A message can have
6// signatures for multiple domains, and the domain does not necessarily have to
7// match a domain in a From header. Receiving mail servers can build a spaminess
8// reputation based on domains that signed the message, along with other
9// mechanisms.
10package dkim
11
12import (
13 "bufio"
14 "bytes"
15 "context"
16 "crypto"
17 "crypto/ed25519"
18 cryptorand "crypto/rand"
19 "crypto/rsa"
20 "errors"
21 "fmt"
22 "hash"
23 "io"
24 "strings"
25 "time"
26
27 "github.com/prometheus/client_golang/prometheus"
28 "github.com/prometheus/client_golang/prometheus/promauto"
29
30 "github.com/mjl-/mox/config"
31 "github.com/mjl-/mox/dns"
32 "github.com/mjl-/mox/mlog"
33 "github.com/mjl-/mox/moxio"
34 "github.com/mjl-/mox/publicsuffix"
35 "github.com/mjl-/mox/smtp"
36)
37
38var xlog = mlog.New("dkim")
39
40var (
41 metricSign = promauto.NewCounterVec(
42 prometheus.CounterOpts{
43 Name: "mox_dkim_sign_total",
44 Help: "DKIM messages signings, label key is the type of key, rsa or ed25519.",
45 },
46 []string{
47 "key",
48 },
49 )
50 metricVerify = promauto.NewHistogramVec(
51 prometheus.HistogramOpts{
52 Name: "mox_dkim_verify_duration_seconds",
53 Help: "DKIM verify, including lookup, duration and result.",
54 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
55 },
56 []string{
57 "algorithm",
58 "status",
59 },
60 )
61)
62
63var timeNow = time.Now // Replaced during tests.
64
65// Status is the result of verifying a DKIM-Signature as described by RFC 8601,
66// "Message Header Field for Indicating Message Authentication Status".
67type Status string
68
69// ../rfc/8601:959 ../rfc/6376:1770 ../rfc/6376:2459
70
71const (
72 StatusNone Status = "none" // Message was not signed.
73 StatusPass Status = "pass" // Message was signed and signature was verified.
74 StatusFail Status = "fail" // Message was signed, but signature was invalid.
75 StatusPolicy Status = "policy" // Message was signed, but signature is not accepted by policy.
76 StatusNeutral Status = "neutral" // Message was signed, but the signature contains an error or could not be processed. This status is also used for errors not covered by other statuses.
77 StatusTemperror Status = "temperror" // Message could not be verified. E.g. because of DNS resolve error. A later attempt may succeed. A missing DNS record is treated as temporary error, a new key may not have propagated through DNS shortly after it was taken into use.
78 StatusPermerror Status = "permerror" // Message cannot be verified. E.g. when a required header field is absent or for invalid (combination of) parameters. Typically set if a DNS record does not allow the signature, e.g. due to algorithm mismatch or expiry.
79)
80
81// Lookup errors.
82var (
83 ErrNoRecord = errors.New("dkim: no dkim dns record for selector and domain")
84 ErrMultipleRecords = errors.New("dkim: multiple dkim dns record for selector and domain")
85 ErrDNS = errors.New("dkim: lookup of dkim dns record")
86 ErrSyntax = errors.New("dkim: syntax error in dkim dns record")
87)
88
89// Signature verification errors.
90var (
91 ErrSigAlgMismatch = errors.New("dkim: signature algorithm mismatch with dns record")
92 ErrHashAlgNotAllowed = errors.New("dkim: hash algorithm not allowed by dns record")
93 ErrKeyNotForEmail = errors.New("dkim: dns record not allowed for use with email")
94 ErrDomainIdentityMismatch = errors.New("dkim: dns record disallows mismatch of domain (d=) and identity (i=)")
95 ErrSigExpired = errors.New("dkim: signature has expired")
96 ErrHashAlgorithmUnknown = errors.New("dkim: unknown hash algorithm")
97 ErrBodyhashMismatch = errors.New("dkim: body hash does not match")
98 ErrSigVerify = errors.New("dkim: signature verification failed")
99 ErrSigAlgorithmUnknown = errors.New("dkim: unknown signature algorithm")
100 ErrCanonicalizationUnknown = errors.New("dkim: unknown canonicalization")
101 ErrHeaderMalformed = errors.New("dkim: mail message header is malformed")
102 ErrFrom = errors.New("dkim: bad from headers")
103 ErrQueryMethod = errors.New("dkim: no recognized query method")
104 ErrKeyRevoked = errors.New("dkim: key has been revoked")
105 ErrTLD = errors.New("dkim: signed domain is top-level domain, above organizational domain")
106 ErrPolicy = errors.New("dkim: signature rejected by policy")
107 ErrWeakKey = errors.New("dkim: key is too weak, need at least 1024 bits for rsa")
108)
109
110// Result is the conclusion of verifying one DKIM-Signature header. An email can
111// have multiple signatures, each with different parameters.
112//
113// To decide what to do with a message, both the signature parameters and the DNS
114// TXT record have to be consulted.
115type Result struct {
116 Status Status
117 Sig *Sig // Parsed form of DKIM-Signature header. Can be nil for invalid DKIM-Signature header.
118 Record *Record // Parsed form of DKIM DNS record for selector and domain in Sig. Optional.
119 RecordAuthentic bool // Whether DKIM DNS record was DNSSEC-protected. Only valid if Sig is non-nil.
120 Err error // If Status is not StatusPass, this error holds the details and can be checked using errors.Is.
121}
122
123// todo: use some io.Writer to hash the body and the header.
124
125// Sign returns line(s) with DKIM-Signature headers, generated according to the configuration.
126func Sign(ctx context.Context, localpart smtp.Localpart, domain dns.Domain, c config.DKIM, smtputf8 bool, msg io.ReaderAt) (headers string, rerr error) {
127 log := xlog.WithContext(ctx)
128 start := timeNow()
129 defer func() {
130 log.Debugx("dkim sign result", rerr, mlog.Field("localpart", localpart), mlog.Field("domain", domain), mlog.Field("smtputf8", smtputf8), mlog.Field("duration", time.Since(start)))
131 }()
132
133 hdrs, bodyOffset, err := parseHeaders(bufio.NewReader(&moxio.AtReader{R: msg}))
134 if err != nil {
135 return "", fmt.Errorf("%w: %s", ErrHeaderMalformed, err)
136 }
137 nfrom := 0
138 for _, h := range hdrs {
139 if h.lkey == "from" {
140 nfrom++
141 }
142 }
143 if nfrom != 1 {
144 return "", fmt.Errorf("%w: message has %d from headers, need exactly 1", ErrFrom, nfrom)
145 }
146
147 type hashKey struct {
148 simple bool // Canonicalization.
149 hash string // lower-case hash.
150 }
151
152 var bodyHashes = map[hashKey][]byte{}
153
154 for _, sign := range c.Sign {
155 sel := c.Selectors[sign]
156 sig := newSigWithDefaults()
157 sig.Version = 1
158 switch sel.Key.(type) {
159 case *rsa.PrivateKey:
160 sig.AlgorithmSign = "rsa"
161 metricSign.WithLabelValues("rsa").Inc()
162 case ed25519.PrivateKey:
163 sig.AlgorithmSign = "ed25519"
164 metricSign.WithLabelValues("ed25519").Inc()
165 default:
166 return "", fmt.Errorf("internal error, unknown pivate key %T", sel.Key)
167 }
168 sig.AlgorithmHash = sel.HashEffective
169 sig.Domain = domain
170 sig.Selector = sel.Domain
171 sig.Identity = &Identity{&localpart, domain}
172 sig.SignedHeaders = append([]string{}, sel.HeadersEffective...)
173 if !sel.DontSealHeaders {
174 // ../rfc/6376:2156
175 // Each time a header name is added to the signature, the next unused value is
176 // signed (in reverse order as they occur in the message). So we can add each
177 // header name as often as it occurs. But now we'll add the header names one
178 // additional time, preventing someone from adding one more header later on.
179 counts := map[string]int{}
180 for _, h := range hdrs {
181 counts[h.lkey]++
182 }
183 for _, h := range sel.HeadersEffective {
184 for j := counts[strings.ToLower(h)]; j > 0; j-- {
185 sig.SignedHeaders = append(sig.SignedHeaders, h)
186 }
187 }
188 }
189 sig.SignTime = timeNow().Unix()
190 if sel.ExpirationSeconds > 0 {
191 sig.ExpireTime = sig.SignTime + int64(sel.ExpirationSeconds)
192 }
193
194 sig.Canonicalization = "simple"
195 if sel.Canonicalization.HeaderRelaxed {
196 sig.Canonicalization = "relaxed"
197 }
198 sig.Canonicalization += "/"
199 if sel.Canonicalization.BodyRelaxed {
200 sig.Canonicalization += "relaxed"
201 } else {
202 sig.Canonicalization += "simple"
203 }
204
205 h, hok := algHash(sig.AlgorithmHash)
206 if !hok {
207 return "", fmt.Errorf("unrecognized hash algorithm %q", sig.AlgorithmHash)
208 }
209
210 // We must now first calculate the hash over the body. Then include that hash in a
211 // new DKIM-Signature header. Then hash that and the signed headers into a data
212 // hash. Then that hash is finally signed and the signature included in the new
213 // DKIM-Signature header.
214 // ../rfc/6376:1700
215
216 hk := hashKey{!sel.Canonicalization.BodyRelaxed, strings.ToLower(sig.AlgorithmHash)}
217 if bh, ok := bodyHashes[hk]; ok {
218 sig.BodyHash = bh
219 } else {
220 br := bufio.NewReader(&moxio.AtReader{R: msg, Offset: int64(bodyOffset)})
221 bh, err = bodyHash(h.New(), !sel.Canonicalization.BodyRelaxed, br)
222 if err != nil {
223 return "", err
224 }
225 sig.BodyHash = bh
226 bodyHashes[hk] = bh
227 }
228
229 sigh, err := sig.Header()
230 if err != nil {
231 return "", err
232 }
233 verifySig := []byte(strings.TrimSuffix(sigh, "\r\n"))
234
235 dh, err := dataHash(h.New(), !sel.Canonicalization.HeaderRelaxed, sig, hdrs, verifySig)
236 if err != nil {
237 return "", err
238 }
239
240 switch key := sel.Key.(type) {
241 case *rsa.PrivateKey:
242 sig.Signature, err = key.Sign(cryptorand.Reader, dh, h)
243 if err != nil {
244 return "", fmt.Errorf("signing data: %v", err)
245 }
246 case ed25519.PrivateKey:
247 // crypto.Hash(0) indicates data isn't prehashed (ed25519ph). We are using
248 // PureEdDSA to sign the sha256 hash. ../rfc/8463:123 ../rfc/8032:427
249 sig.Signature, err = key.Sign(cryptorand.Reader, dh, crypto.Hash(0))
250 if err != nil {
251 return "", fmt.Errorf("signing data: %v", err)
252 }
253 default:
254 return "", fmt.Errorf("unsupported private key type: %s", err)
255 }
256
257 sigh, err = sig.Header()
258 if err != nil {
259 return "", err
260 }
261 headers += sigh
262 }
263
264 return headers, nil
265}
266
267// Lookup looks up the DKIM TXT record and parses it.
268//
269// A requested record is <selector>._domainkey.<domain>. Exactly one valid DKIM
270// record should be present.
271//
272// authentic indicates if DNS results were DNSSEC-verified.
273func Lookup(ctx context.Context, resolver dns.Resolver, selector, domain dns.Domain) (rstatus Status, rrecord *Record, rtxt string, authentic bool, rerr error) {
274 log := xlog.WithContext(ctx)
275 start := timeNow()
276 defer func() {
277 log.Debugx("dkim lookup result", rerr, mlog.Field("selector", selector), mlog.Field("domain", domain), mlog.Field("status", rstatus), mlog.Field("record", rrecord), mlog.Field("duration", time.Since(start)))
278 }()
279
280 name := selector.ASCII + "._domainkey." + domain.ASCII + "."
281 records, lookupResult, err := dns.WithPackage(resolver, "dkim").LookupTXT(ctx, name)
282 if dns.IsNotFound(err) {
283 // ../rfc/6376:2608
284 // We must return StatusPermerror. We may want to return StatusTemperror because in
285 // practice someone will start using a new key before DNS changes have propagated.
286 return StatusPermerror, nil, "", lookupResult.Authentic, fmt.Errorf("%w: dns name %q", ErrNoRecord, name)
287 } else if err != nil {
288 return StatusTemperror, nil, "", lookupResult.Authentic, fmt.Errorf("%w: dns name %q: %s", ErrDNS, name, err)
289 }
290
291 // ../rfc/6376:2612
292 var status = StatusTemperror
293 var record *Record
294 var txt string
295 err = nil
296 for _, s := range records {
297 // We interpret ../rfc/6376:2621 to mean that a record that claims to be v=DKIM1,
298 // but isn't actually valid, results in a StatusPermFail. But a record that isn't
299 // claiming to be DKIM1 is ignored.
300 var r *Record
301 var isdkim bool
302 r, isdkim, err = ParseRecord(s)
303 if err != nil && isdkim {
304 return StatusPermerror, nil, txt, lookupResult.Authentic, fmt.Errorf("%w: %s", ErrSyntax, err)
305 } else if err != nil {
306 // Hopefully the remote MTA admin discovers the configuration error and fix it for
307 // an upcoming delivery attempt, in case we rejected with temporary status.
308 status = StatusTemperror
309 err = fmt.Errorf("%w: not a dkim record: %s", ErrSyntax, err)
310 continue
311 }
312 // If there are multiple valid records, return a temporary error. Perhaps the error is fixed soon.
313 // ../rfc/6376:1609
314 // ../rfc/6376:2584
315 if record != nil {
316 return StatusTemperror, nil, "", lookupResult.Authentic, fmt.Errorf("%w: dns name %q", ErrMultipleRecords, name)
317 }
318 record = r
319 txt = s
320 err = nil
321 }
322
323 if record == nil {
324 return status, nil, "", lookupResult.Authentic, err
325 }
326 return StatusNeutral, record, txt, lookupResult.Authentic, nil
327}
328
329// Verify parses the DKIM-Signature headers in a message and verifies each of them.
330//
331// If the headers of the message cannot be found, an error is returned.
332// Otherwise, each DKIM-Signature header is reflected in the returned results.
333//
334// NOTE: Verify does not check if the domain (d=) that signed the message is
335// the domain of the sender. The caller, e.g. through DMARC, should do this.
336//
337// If ignoreTestMode is true and the DKIM record is in test mode (t=y), a
338// verification failure is treated as actual failure. With ignoreTestMode
339// false, such verification failures are treated as if there is no signature by
340// returning StatusNone.
341func Verify(ctx context.Context, resolver dns.Resolver, smtputf8 bool, policy func(*Sig) error, r io.ReaderAt, ignoreTestMode bool) (results []Result, rerr error) {
342 log := xlog.WithContext(ctx)
343 start := timeNow()
344 defer func() {
345 duration := float64(time.Since(start)) / float64(time.Second)
346 for _, r := range results {
347 var alg string
348 if r.Sig != nil {
349 alg = r.Sig.Algorithm()
350 }
351 status := string(r.Status)
352 metricVerify.WithLabelValues(alg, status).Observe(duration)
353 }
354
355 if len(results) == 0 {
356 log.Debugx("dkim verify result", rerr, mlog.Field("smtputf8", smtputf8), mlog.Field("duration", time.Since(start)))
357 }
358 for _, result := range results {
359 log.Debugx("dkim verify result", result.Err, mlog.Field("smtputf8", smtputf8), mlog.Field("status", result.Status), mlog.Field("sig", result.Sig), mlog.Field("record", result.Record), mlog.Field("duration", time.Since(start)))
360 }
361 }()
362
363 hdrs, bodyOffset, err := parseHeaders(bufio.NewReader(&moxio.AtReader{R: r}))
364 if err != nil {
365 return nil, fmt.Errorf("%w: %s", ErrHeaderMalformed, err)
366 }
367
368 // todo: reuse body hashes and possibly verify signatures in parallel. and start the dns lookup immediately. ../rfc/6376:2697
369
370 for _, h := range hdrs {
371 if h.lkey != "dkim-signature" {
372 continue
373 }
374
375 sig, verifySig, err := parseSignature(h.raw, smtputf8)
376 if err != nil {
377 // ../rfc/6376:2503
378 err := fmt.Errorf("parsing DKIM-Signature header: %w", err)
379 results = append(results, Result{StatusPermerror, nil, nil, false, err})
380 continue
381 }
382
383 h, canonHeaderSimple, canonDataSimple, err := checkSignatureParams(ctx, sig)
384 if err != nil {
385 results = append(results, Result{StatusPermerror, sig, nil, false, err})
386 continue
387 }
388
389 // ../rfc/6376:2560
390 if err := policy(sig); err != nil {
391 err := fmt.Errorf("%w: %s", ErrPolicy, err)
392 results = append(results, Result{StatusPolicy, sig, nil, false, err})
393 continue
394 }
395
396 br := bufio.NewReader(&moxio.AtReader{R: r, Offset: int64(bodyOffset)})
397 status, txt, authentic, err := verifySignature(ctx, resolver, sig, h, canonHeaderSimple, canonDataSimple, hdrs, verifySig, br, ignoreTestMode)
398 results = append(results, Result{status, sig, txt, authentic, err})
399 }
400 return results, nil
401}
402
403// check if signature is acceptable.
404// Only looks at the signature parameters, not at the DNS record.
405func checkSignatureParams(ctx context.Context, sig *Sig) (hash crypto.Hash, canonHeaderSimple, canonBodySimple bool, rerr error) {
406 // "From" header is required, ../rfc/6376:2122 ../rfc/6376:2546
407 var from bool
408 for _, h := range sig.SignedHeaders {
409 if strings.EqualFold(h, "from") {
410 from = true
411 break
412 }
413 }
414 if !from {
415 return 0, false, false, fmt.Errorf(`%w: required "from" header not signed`, ErrFrom)
416 }
417
418 // ../rfc/6376:2550
419 if sig.ExpireTime >= 0 && sig.ExpireTime < timeNow().Unix() {
420 return 0, false, false, fmt.Errorf("%w: expiration time %q", ErrSigExpired, time.Unix(sig.ExpireTime, 0).Format(time.RFC3339))
421 }
422
423 // ../rfc/6376:2554
424 // ../rfc/6376:3284
425 // Refuse signatures that reach beyond declared scope. We use the existing
426 // publicsuffix.Lookup to lookup a fake subdomain of the signing domain. If this
427 // supposed subdomain is actually an organizational domain, the signing domain
428 // shouldn't be signing for its organizational domain.
429 subdom := sig.Domain
430 subdom.ASCII = "x." + subdom.ASCII
431 if subdom.Unicode != "" {
432 subdom.Unicode = "x." + subdom.Unicode
433 }
434 if orgDom := publicsuffix.Lookup(ctx, subdom); subdom.ASCII == orgDom.ASCII {
435 return 0, false, false, fmt.Errorf("%w: %s", ErrTLD, sig.Domain)
436 }
437
438 h, hok := algHash(sig.AlgorithmHash)
439 if !hok {
440 return 0, false, false, fmt.Errorf("%w: %q", ErrHashAlgorithmUnknown, sig.AlgorithmHash)
441 }
442
443 t := strings.SplitN(sig.Canonicalization, "/", 2)
444
445 switch strings.ToLower(t[0]) {
446 case "simple":
447 canonHeaderSimple = true
448 case "relaxed":
449 default:
450 return 0, false, false, fmt.Errorf("%w: header canonicalization %q", ErrCanonicalizationUnknown, sig.Canonicalization)
451 }
452
453 canon := "simple"
454 if len(t) == 2 {
455 canon = t[1]
456 }
457 switch strings.ToLower(canon) {
458 case "simple":
459 canonBodySimple = true
460 case "relaxed":
461 default:
462 return 0, false, false, fmt.Errorf("%w: body canonicalization %q", ErrCanonicalizationUnknown, sig.Canonicalization)
463 }
464
465 // We only recognize query method dns/txt, which is the default. ../rfc/6376:1268
466 if len(sig.QueryMethods) > 0 {
467 var dnstxt bool
468 for _, m := range sig.QueryMethods {
469 if strings.EqualFold(m, "dns/txt") {
470 dnstxt = true
471 break
472 }
473 }
474 if !dnstxt {
475 return 0, false, false, fmt.Errorf("%w: need dns/txt", ErrQueryMethod)
476 }
477 }
478
479 return h, canonHeaderSimple, canonBodySimple, nil
480}
481
482// lookup the public key in the DNS and verify the signature.
483func verifySignature(ctx context.Context, resolver dns.Resolver, sig *Sig, hash crypto.Hash, canonHeaderSimple, canonDataSimple bool, hdrs []header, verifySig []byte, body *bufio.Reader, ignoreTestMode bool) (Status, *Record, bool, error) {
484 // ../rfc/6376:2604
485 status, record, _, authentic, err := Lookup(ctx, resolver, sig.Selector, sig.Domain)
486 if err != nil {
487 // todo: for temporary errors, we could pass on information so caller returns a 4.7.5 ecode, ../rfc/6376:2777
488 return status, nil, authentic, err
489 }
490 status, err = verifySignatureRecord(record, sig, hash, canonHeaderSimple, canonDataSimple, hdrs, verifySig, body, ignoreTestMode)
491 return status, record, authentic, err
492}
493
494// verify a DKIM signature given the record from dns and signature from the email message.
495func verifySignatureRecord(r *Record, sig *Sig, hash crypto.Hash, canonHeaderSimple, canonDataSimple bool, hdrs []header, verifySig []byte, body *bufio.Reader, ignoreTestMode bool) (rstatus Status, rerr error) {
496 if !ignoreTestMode {
497 // ../rfc/6376:1558
498 y := false
499 for _, f := range r.Flags {
500 if strings.EqualFold(f, "y") {
501 y = true
502 break
503 }
504 }
505 if y {
506 defer func() {
507 if rstatus != StatusPass {
508 rstatus = StatusNone
509 }
510 }()
511 }
512 }
513
514 // ../rfc/6376:2639
515 if len(r.Hashes) > 0 {
516 ok := false
517 for _, h := range r.Hashes {
518 if strings.EqualFold(h, sig.AlgorithmHash) {
519 ok = true
520 break
521 }
522 }
523 if !ok {
524 return StatusPermerror, fmt.Errorf("%w: dkim dns record expects one of %q, message uses %q", ErrHashAlgNotAllowed, strings.Join(r.Hashes, ","), sig.AlgorithmHash)
525 }
526 }
527
528 // ../rfc/6376:2651
529 if !strings.EqualFold(r.Key, sig.AlgorithmSign) {
530 return StatusPermerror, fmt.Errorf("%w: dkim dns record requires algorithm %q, message has %q", ErrSigAlgMismatch, r.Key, sig.AlgorithmSign)
531 }
532
533 // ../rfc/6376:2645
534 if r.PublicKey == nil {
535 return StatusPermerror, ErrKeyRevoked
536 } else if rsaKey, ok := r.PublicKey.(*rsa.PublicKey); ok && rsaKey.N.BitLen() < 1024 {
537 // todo: find a reference that supports this.
538 return StatusPermerror, ErrWeakKey
539 }
540
541 // ../rfc/6376:1541
542 if !r.ServiceAllowed("email") {
543 return StatusPermerror, ErrKeyNotForEmail
544 }
545 for _, t := range r.Flags {
546 // ../rfc/6376:1575
547 // ../rfc/6376:1805
548 if strings.EqualFold(t, "s") && sig.Identity != nil {
549 if sig.Identity.Domain.ASCII != sig.Domain.ASCII {
550 return StatusPermerror, fmt.Errorf("%w: i= identity domain %q must match d= domain %q", ErrDomainIdentityMismatch, sig.Domain.ASCII, sig.Identity.Domain.ASCII)
551 }
552 }
553 }
554
555 if sig.Length >= 0 {
556 // todo future: implement l= parameter in signatures. we don't currently allow this through policy check.
557 return StatusPermerror, fmt.Errorf("l= (length) parameter in signature not yet implemented")
558 }
559
560 // We first check the signature is with the claimed body hash is valid. Then we
561 // verify the body hash. In case of invalid signatures, we won't read the entire
562 // body.
563 // ../rfc/6376:1700
564 // ../rfc/6376:2656
565
566 dh, err := dataHash(hash.New(), canonHeaderSimple, sig, hdrs, verifySig)
567 if err != nil {
568 // Any error is likely an invalid header field in the message, hence permanent error.
569 return StatusPermerror, fmt.Errorf("calculating data hash: %w", err)
570 }
571
572 switch k := r.PublicKey.(type) {
573 case *rsa.PublicKey:
574 if err := rsa.VerifyPKCS1v15(k, hash, dh, sig.Signature); err != nil {
575 return StatusFail, fmt.Errorf("%w: rsa verification: %s", ErrSigVerify, err)
576 }
577 case ed25519.PublicKey:
578 if ok := ed25519.Verify(k, dh, sig.Signature); !ok {
579 return StatusFail, fmt.Errorf("%w: ed25519 verification", ErrSigVerify)
580 }
581 default:
582 return StatusPermerror, fmt.Errorf("%w: unrecognized signature algorithm %q", ErrSigAlgorithmUnknown, r.Key)
583 }
584
585 bh, err := bodyHash(hash.New(), canonDataSimple, body)
586 if err != nil {
587 // Any error is likely some internal error, hence temporary error.
588 return StatusTemperror, fmt.Errorf("calculating body hash: %w", err)
589 }
590 if !bytes.Equal(sig.BodyHash, bh) {
591 return StatusFail, fmt.Errorf("%w: signature bodyhash %x != calculated bodyhash %x", ErrBodyhashMismatch, sig.BodyHash, bh)
592 }
593
594 return StatusPass, nil
595}
596
597func algHash(s string) (crypto.Hash, bool) {
598 if strings.EqualFold(s, "sha1") {
599 return crypto.SHA1, true
600 } else if strings.EqualFold(s, "sha256") {
601 return crypto.SHA256, true
602 }
603 return 0, false
604}
605
606// bodyHash calculates the hash over the body.
607func bodyHash(h hash.Hash, canonSimple bool, body *bufio.Reader) ([]byte, error) {
608 // todo: take l= into account. we don't currently allow it for policy reasons.
609
610 var crlf = []byte("\r\n")
611
612 if canonSimple {
613 // ../rfc/6376:864, ensure body ends with exactly one trailing crlf.
614 ncrlf := 0
615 for {
616 buf, err := body.ReadBytes('\n')
617 if len(buf) == 0 && err == io.EOF {
618 break
619 }
620 if err != nil && err != io.EOF {
621 return nil, err
622 }
623 hascrlf := bytes.HasSuffix(buf, crlf)
624 if hascrlf {
625 buf = buf[:len(buf)-2]
626 }
627 if len(buf) > 0 {
628 for ; ncrlf > 0; ncrlf-- {
629 h.Write(crlf)
630 }
631 h.Write(buf)
632 }
633 if hascrlf {
634 ncrlf++
635 }
636 }
637 h.Write(crlf)
638 } else {
639 hb := bufio.NewWriter(h)
640
641 // We go through the body line by line, replacing WSP with a single space and removing whitespace at the end of lines.
642 // We stash "empty" lines. If they turn out to be at the end of the file, we must drop them.
643 stash := &bytes.Buffer{}
644 var line bool // Whether buffer read is for continuation of line.
645 var prev byte // Previous byte read for line.
646 linesEmpty := true // Whether stash contains only empty lines and may need to be dropped.
647 var bodynonempty bool // Whether body is non-empty, for adding missing crlf.
648 var hascrlf bool // Whether current/last line ends with crlf, for adding missing crlf.
649 for {
650 // todo: should not read line at a time, count empty lines. reduces max memory usage. a message with lots of empty lines can cause high memory use.
651 buf, err := body.ReadBytes('\n')
652 if len(buf) == 0 && err == io.EOF {
653 break
654 }
655 if err != nil && err != io.EOF {
656 return nil, err
657 }
658 bodynonempty = true
659
660 hascrlf = bytes.HasSuffix(buf, crlf)
661 if hascrlf {
662 buf = buf[:len(buf)-2]
663
664 // ../rfc/6376:893, "ignore all whitespace at the end of lines".
665 // todo: what is "whitespace"? it isn't WSP (space and tab), the next line mentions WSP explicitly for another rule. should we drop trailing \r, \n, \v, more?
666 buf = bytes.TrimRight(buf, " \t")
667 }
668
669 // Replace one or more WSP to a single SP.
670 for i, c := range buf {
671 wsp := c == ' ' || c == '\t'
672 if (i >= 0 || line) && wsp {
673 if prev == ' ' {
674 continue
675 }
676 prev = ' '
677 c = ' '
678 } else {
679 prev = c
680 }
681 if !wsp {
682 linesEmpty = false
683 }
684 stash.WriteByte(c)
685 }
686 if hascrlf {
687 stash.Write(crlf)
688 }
689 line = !hascrlf
690 if !linesEmpty {
691 hb.Write(stash.Bytes())
692 stash.Reset()
693 linesEmpty = true
694 }
695 }
696 // ../rfc/6376:886
697 // Only for non-empty bodies without trailing crlf do we add the missing crlf.
698 if bodynonempty && !hascrlf {
699 hb.Write(crlf)
700 }
701
702 hb.Flush()
703 }
704 return h.Sum(nil), nil
705}
706
707func dataHash(h hash.Hash, canonSimple bool, sig *Sig, hdrs []header, verifySig []byte) ([]byte, error) {
708 headers := ""
709 revHdrs := map[string][]header{}
710 for _, h := range hdrs {
711 revHdrs[h.lkey] = append([]header{h}, revHdrs[h.lkey]...)
712 }
713
714 for _, key := range sig.SignedHeaders {
715 lkey := strings.ToLower(key)
716 h := revHdrs[lkey]
717 if len(h) == 0 {
718 continue
719 }
720 revHdrs[lkey] = h[1:]
721 s := string(h[0].raw)
722 if canonSimple {
723 // ../rfc/6376:823
724 // Add unmodified.
725 headers += s
726 } else {
727 ch, err := relaxedCanonicalHeaderWithoutCRLF(s)
728 if err != nil {
729 return nil, fmt.Errorf("canonicalizing header: %w", err)
730 }
731 headers += ch + "\r\n"
732 }
733 }
734 // ../rfc/6376:2377, canonicalization does not apply to the dkim-signature header.
735 h.Write([]byte(headers))
736 dkimSig := verifySig
737 if !canonSimple {
738 ch, err := relaxedCanonicalHeaderWithoutCRLF(string(verifySig))
739 if err != nil {
740 return nil, fmt.Errorf("canonicalizing DKIM-Signature header: %w", err)
741 }
742 dkimSig = []byte(ch)
743 }
744 h.Write(dkimSig)
745 return h.Sum(nil), nil
746}
747
748// a single header, can be multiline.
749func relaxedCanonicalHeaderWithoutCRLF(s string) (string, error) {
750 // ../rfc/6376:831
751 t := strings.SplitN(s, ":", 2)
752 if len(t) != 2 {
753 return "", fmt.Errorf("%w: invalid header %q", ErrHeaderMalformed, s)
754 }
755
756 // Unfold, we keep the leading WSP on continuation lines and fix it up below.
757 v := strings.ReplaceAll(t[1], "\r\n", "")
758
759 // Replace one or more WSP to a single SP.
760 var nv []byte
761 var prev byte
762 for i, c := range []byte(v) {
763 if i >= 0 && c == ' ' || c == '\t' {
764 if prev == ' ' {
765 continue
766 }
767 prev = ' '
768 c = ' '
769 } else {
770 prev = c
771 }
772 nv = append(nv, c)
773 }
774
775 ch := strings.ToLower(strings.TrimRight(t[0], " \t")) + ":" + strings.Trim(string(nv), " \t")
776 return ch, nil
777}
778
779type header struct {
780 key string // Key in original case.
781 lkey string // Key in lower-case, for canonical case.
782 value []byte // Literal header value, possibly spanning multiple lines, not modified in any way, including crlf, excluding leading key and colon.
783 raw []byte // Like value, but including original leading key and colon. Ready for use as simple header canonicalized use.
784}
785
786func parseHeaders(br *bufio.Reader) ([]header, int, error) {
787 var o int
788 var l []header
789 var key, lkey string
790 var value []byte
791 var raw []byte
792 for {
793 line, err := readline(br)
794 if err != nil {
795 return nil, 0, err
796 }
797 o += len(line)
798 if bytes.Equal(line, []byte("\r\n")) {
799 break
800 }
801 if line[0] == ' ' || line[0] == '\t' {
802 if len(l) == 0 && key == "" {
803 return nil, 0, fmt.Errorf("malformed message, starts with space/tab")
804 }
805 value = append(value, line...)
806 raw = append(raw, line...)
807 continue
808 }
809 if key != "" {
810 l = append(l, header{key, lkey, value, raw})
811 }
812 t := bytes.SplitN(line, []byte(":"), 2)
813 if len(t) != 2 {
814 return nil, 0, fmt.Errorf("malformed message, header without colon")
815 }
816
817 key = strings.TrimRight(string(t[0]), " \t") // todo: where is this specified?
818 // Check for valid characters. ../rfc/5322:1689 ../rfc/6532:193
819 for _, c := range key {
820 if c <= ' ' || c >= 0x7f {
821 return nil, 0, fmt.Errorf("invalid header field name")
822 }
823 }
824 if key == "" {
825 return nil, 0, fmt.Errorf("empty header key")
826 }
827 lkey = strings.ToLower(key)
828 value = append([]byte{}, t[1]...)
829 raw = append([]byte{}, line...)
830 }
831 if key != "" {
832 l = append(l, header{key, lkey, value, raw})
833 }
834 return l, o, nil
835}
836
837func readline(r *bufio.Reader) ([]byte, error) {
838 var buf []byte
839 for {
840 line, err := r.ReadBytes('\n')
841 if err != nil {
842 return nil, err
843 }
844 if bytes.HasSuffix(line, []byte("\r\n")) {
845 if len(buf) == 0 {
846 return line, nil
847 }
848 return append(buf, line...), nil
849 }
850 buf = append(buf, line...)
851 }
852}
853