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