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