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