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