2023-01-30 16:27:06 +03:00
// Package dkim (DomainKeys Identified Mail signatures, RFC 6376) signs and
// verifies DKIM signatures.
//
// Signatures are added to email messages in DKIM-Signature headers. By signing a
// message, a domain takes responsibility for the message. A message can have
// signatures for multiple domains, and the domain does not necessarily have to
// match a domain in a From header. Receiving mail servers can build a spaminess
// reputation based on domains that signed the message, along with other
// mechanisms.
package dkim
import (
"bufio"
"bytes"
"context"
"crypto"
"crypto/ed25519"
cryptorand "crypto/rand"
"crypto/rsa"
"errors"
"fmt"
"hash"
"io"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/mjl-/mox/config"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/moxio"
"github.com/mjl-/mox/publicsuffix"
"github.com/mjl-/mox/smtp"
)
var xlog = mlog . New ( "dkim" )
var (
metricDKIMSign = promauto . NewCounterVec (
prometheus . CounterOpts {
Name : "mox_dkim_sign_total" ,
2023-09-15 17:47:17 +03:00
Help : "DKIM messages signings, label key is the type of key, rsa or ed25519." ,
2023-01-30 16:27:06 +03:00
} ,
[ ] string {
"key" ,
} ,
)
metricDKIMVerify = promauto . NewHistogramVec (
prometheus . HistogramOpts {
Name : "mox_dkim_verify_duration_seconds" ,
Help : "DKIM verify, including lookup, duration and result." ,
Buckets : [ ] float64 { 0.001 , 0.005 , 0.01 , 0.05 , 0.100 , 0.5 , 1 , 5 , 10 , 20 } ,
} ,
[ ] string {
"algorithm" ,
"status" ,
} ,
)
)
var timeNow = time . Now // Replaced during tests.
// Status is the result of verifying a DKIM-Signature as described by RFC 8601,
// "Message Header Field for Indicating Message Authentication Status".
type Status string
// ../rfc/8601:959 ../rfc/6376:1770 ../rfc/6376:2459
const (
StatusNone Status = "none" // Message was not signed.
StatusPass Status = "pass" // Message was signed and signature was verified.
StatusFail Status = "fail" // Message was signed, but signature was invalid.
StatusPolicy Status = "policy" // Message was signed, but signature is not accepted by policy.
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.
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.
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.
)
// Lookup errors.
var (
ErrNoRecord = errors . New ( "dkim: no dkim dns record for selector and domain" )
ErrMultipleRecords = errors . New ( "dkim: multiple dkim dns record for selector and domain" )
ErrDNS = errors . New ( "dkim: lookup of dkim dns record" )
ErrSyntax = errors . New ( "dkim: syntax error in dkim dns record" )
)
// Signature verification errors.
var (
ErrSigAlgMismatch = errors . New ( "dkim: signature algorithm mismatch with dns record" )
ErrHashAlgNotAllowed = errors . New ( "dkim: hash algorithm not allowed by dns record" )
ErrKeyNotForEmail = errors . New ( "dkim: dns record not allowed for use with email" )
ErrDomainIdentityMismatch = errors . New ( "dkim: dns record disallows mismatch of domain (d=) and identity (i=)" )
ErrSigExpired = errors . New ( "dkim: signature has expired" )
ErrHashAlgorithmUnknown = errors . New ( "dkim: unknown hash algorithm" )
ErrBodyhashMismatch = errors . New ( "dkim: body hash does not match" )
ErrSigVerify = errors . New ( "dkim: signature verification failed" )
ErrSigAlgorithmUnknown = errors . New ( "dkim: unknown signature algorithm" )
ErrCanonicalizationUnknown = errors . New ( "dkim: unknown canonicalization" )
ErrHeaderMalformed = errors . New ( "dkim: mail message header is malformed" )
ErrFrom = errors . New ( "dkim: bad from headers" )
ErrQueryMethod = errors . New ( "dkim: no recognized query method" )
ErrKeyRevoked = errors . New ( "dkim: key has been revoked" )
ErrTLD = errors . New ( "dkim: signed domain is top-level domain, above organizational domain" )
ErrPolicy = errors . New ( "dkim: signature rejected by policy" )
ErrWeakKey = errors . New ( "dkim: key is too weak, need at least 1024 bits for rsa" )
)
// Result is the conclusion of verifying one DKIM-Signature header. An email can
// have multiple signatures, each with different parameters.
//
// To decide what to do with a message, both the signature parameters and the DNS
// TXT record have to be consulted.
type Result struct {
Status Status
Sig * Sig // Parsed form of DKIM-Signature header. Can be nil for invalid DKIM-Signature header.
Record * Record // Parsed form of DKIM DNS record for selector and domain in Sig. Optional.
Err error // If Status is not StatusPass, this error holds the details and can be checked using errors.Is.
}
// todo: use some io.Writer to hash the body and the header.
// Sign returns line(s) with DKIM-Signature headers, generated according to the configuration.
func Sign ( ctx context . Context , localpart smtp . Localpart , domain dns . Domain , c config . DKIM , smtputf8 bool , msg io . ReaderAt ) ( headers string , rerr error ) {
log := xlog . WithContext ( ctx )
start := timeNow ( )
defer func ( ) {
log . Debugx ( "dkim sign result" , rerr , mlog . Field ( "localpart" , localpart ) , mlog . Field ( "domain" , domain ) , mlog . Field ( "smtputf8" , smtputf8 ) , mlog . Field ( "duration" , time . Since ( start ) ) )
} ( )
hdrs , bodyOffset , err := parseHeaders ( bufio . NewReader ( & moxio . AtReader { R : msg } ) )
if err != nil {
return "" , fmt . Errorf ( "%w: %s" , ErrHeaderMalformed , err )
}
nfrom := 0
for _ , h := range hdrs {
if h . lkey == "from" {
nfrom ++
}
}
if nfrom != 1 {
return "" , fmt . Errorf ( "%w: message has %d from headers, need exactly 1" , ErrFrom , nfrom )
}
type hashKey struct {
simple bool // Canonicalization.
hash string // lower-case hash.
}
var bodyHashes = map [ hashKey ] [ ] byte { }
for _ , sign := range c . Sign {
sel := c . Selectors [ sign ]
sig := newSigWithDefaults ( )
sig . Version = 1
switch sel . Key . ( type ) {
case * rsa . PrivateKey :
sig . AlgorithmSign = "rsa"
metricDKIMSign . WithLabelValues ( "rsa" ) . Inc ( )
case ed25519 . PrivateKey :
sig . AlgorithmSign = "ed25519"
metricDKIMSign . WithLabelValues ( "ed25519" ) . Inc ( )
default :
return "" , fmt . Errorf ( "internal error, unknown pivate key %T" , sel . Key )
}
sig . AlgorithmHash = sel . HashEffective
sig . Domain = domain
sig . Selector = sel . Domain
sig . Identity = & Identity { & localpart , domain }
sig . SignedHeaders = append ( [ ] string { } , sel . HeadersEffective ... )
if ! sel . DontSealHeaders {
// ../rfc/6376:2156
// Each time a header name is added to the signature, the next unused value is
// signed (in reverse order as they occur in the message). So we can add each
// header name as often as it occurs. But now we'll add the header names one
// additional time, preventing someone from adding one more header later on.
counts := map [ string ] int { }
for _ , h := range hdrs {
counts [ h . lkey ] ++
}
for _ , h := range sel . HeadersEffective {
for j := counts [ strings . ToLower ( h ) ] ; j > 0 ; j -- {
sig . SignedHeaders = append ( sig . SignedHeaders , h )
}
}
}
sig . SignTime = timeNow ( ) . Unix ( )
if sel . ExpirationSeconds > 0 {
sig . ExpireTime = sig . SignTime + int64 ( sel . ExpirationSeconds )
}
sig . Canonicalization = "simple"
if sel . Canonicalization . HeaderRelaxed {
sig . Canonicalization = "relaxed"
}
sig . Canonicalization += "/"
if sel . Canonicalization . BodyRelaxed {
sig . Canonicalization += "relaxed"
} else {
sig . Canonicalization += "simple"
}
h , hok := algHash ( sig . AlgorithmHash )
if ! hok {
return "" , fmt . Errorf ( "unrecognized hash algorithm %q" , sig . AlgorithmHash )
}
// We must now first calculate the hash over the body. Then include that hash in a
// new DKIM-Signature header. Then hash that and the signed headers into a data
// hash. Then that hash is finally signed and the signature included in the new
// DKIM-Signature header.
// ../rfc/6376:1700
hk := hashKey { ! sel . Canonicalization . BodyRelaxed , strings . ToLower ( sig . AlgorithmHash ) }
if bh , ok := bodyHashes [ hk ] ; ok {
sig . BodyHash = bh
} else {
br := bufio . NewReader ( & moxio . AtReader { R : msg , Offset : int64 ( bodyOffset ) } )
bh , err = bodyHash ( h . New ( ) , ! sel . Canonicalization . BodyRelaxed , br )
if err != nil {
return "" , err
}
sig . BodyHash = bh
bodyHashes [ hk ] = bh
}
sigh , err := sig . Header ( )
if err != nil {
return "" , err
}
verifySig := [ ] byte ( strings . TrimSuffix ( sigh , "\r\n" ) )
dh , err := dataHash ( h . New ( ) , ! sel . Canonicalization . HeaderRelaxed , sig , hdrs , verifySig )
if err != nil {
return "" , err
}
switch key := sel . Key . ( type ) {
case * rsa . PrivateKey :
sig . Signature , err = key . Sign ( cryptorand . Reader , dh , h )
if err != nil {
return "" , fmt . Errorf ( "signing data: %v" , err )
}
case ed25519 . PrivateKey :
// crypto.Hash(0) indicates data isn't prehashed (ed25519ph). We are using
// PureEdDSA to sign the sha256 hash. ../rfc/8463:123 ../rfc/8032:427
sig . Signature , err = key . Sign ( cryptorand . Reader , dh , crypto . Hash ( 0 ) )
if err != nil {
return "" , fmt . Errorf ( "signing data: %v" , err )
}
default :
return "" , fmt . Errorf ( "unsupported private key type: %s" , err )
}
sigh , err = sig . Header ( )
if err != nil {
return "" , err
}
headers += sigh
}
return headers , nil
}
// Lookup looks up the DKIM TXT record and parses it.
//
// A requested record is <selector>._domainkey.<domain>. Exactly one valid DKIM
// record should be present.
func Lookup ( ctx context . Context , resolver dns . Resolver , selector , domain dns . Domain ) ( rstatus Status , rrecord * Record , rtxt string , rerr error ) {
log := xlog . WithContext ( ctx )
start := timeNow ( )
defer func ( ) {
log . Debugx ( "dkim lookup result" , rerr , mlog . Field ( "selector" , selector ) , mlog . Field ( "domain" , domain ) , mlog . Field ( "status" , rstatus ) , mlog . Field ( "record" , rrecord ) , mlog . Field ( "duration" , time . Since ( start ) ) )
} ( )
name := selector . ASCII + "._domainkey." + domain . ASCII + "."
records , err := dns . WithPackage ( resolver , "dkim" ) . LookupTXT ( ctx , name )
if dns . IsNotFound ( err ) {
// ../rfc/6376:2608
// We must return StatusPermerror. We may want to return StatusTemperror because in
// practice someone will start using a new key before DNS changes have propagated.
return StatusPermerror , nil , "" , fmt . Errorf ( "%w: dns name %q" , ErrNoRecord , name )
} else if err != nil {
return StatusTemperror , nil , "" , fmt . Errorf ( "%w: dns name %q: %s" , ErrDNS , name , err )
}
// ../rfc/6376:2612
var status = StatusTemperror
var record * Record
var txt string
err = nil
for _ , s := range records {
// We interpret ../rfc/6376:2621 to mean that a record that claims to be v=DKIM1,
// but isn't actually valid, results in a StatusPermFail. But a record that isn't
// claiming to be DKIM1 is ignored.
var r * Record
var isdkim bool
r , isdkim , err = ParseRecord ( s )
if err != nil && isdkim {
return StatusPermerror , nil , txt , fmt . Errorf ( "%w: %s" , ErrSyntax , err )
} else if err != nil {
// Hopefully the remote MTA admin discovers the configuration error and fix it for
// an upcoming delivery attempt, in case we rejected with temporary status.
status = StatusTemperror
err = fmt . Errorf ( "%w: not a dkim record: %s" , ErrSyntax , err )
continue
}
// If there are multiple valid records, return a temporary error. Perhaps the error is fixed soon.
// ../rfc/6376:1609
// ../rfc/6376:2584
if record != nil {
return StatusTemperror , nil , "" , fmt . Errorf ( "%w: dns name %q" , ErrMultipleRecords , name )
}
record = r
txt = s
err = nil
}
if record == nil {
return status , nil , "" , err
}
return StatusNeutral , record , txt , nil
}
// Verify parses the DKIM-Signature headers in a message and verifies each of them.
//
// If the headers of the message cannot be found, an error is returned.
// Otherwise, each DKIM-Signature header is reflected in the returned results.
//
// NOTE: Verify does not check if the domain (d=) that signed the message is
// the domain of the sender. The caller, e.g. through DMARC, should do this.
//
// If ignoreTestMode is true and the DKIM record is in test mode (t=y), a
// verification failure is treated as actual failure. With ignoreTestMode
// false, such verification failures are treated as if there is no signature by
// returning StatusNone.
func Verify ( ctx context . Context , resolver dns . Resolver , smtputf8 bool , policy func ( * Sig ) error , r io . ReaderAt , ignoreTestMode bool ) ( results [ ] Result , rerr error ) {
log := xlog . WithContext ( ctx )
start := timeNow ( )
defer func ( ) {
duration := float64 ( time . Since ( start ) ) / float64 ( time . Second )
for _ , r := range results {
var alg string
if r . Sig != nil {
alg = r . Sig . Algorithm ( )
}
status := string ( r . Status )
metricDKIMVerify . WithLabelValues ( alg , status ) . Observe ( duration )
}
if len ( results ) == 0 {
log . Debugx ( "dkim verify result" , rerr , mlog . Field ( "smtputf8" , smtputf8 ) , mlog . Field ( "duration" , time . Since ( start ) ) )
}
for _ , result := range results {
log . Debugx ( "dkim verify result" , result . Err , mlog . Field ( "smtputf8" , smtputf8 ) , mlog . Field ( "status" , result . Status ) , mlog . Field ( "sig" , result . Sig ) , mlog . Field ( "record" , result . Record ) , mlog . Field ( "duration" , time . Since ( start ) ) )
}
} ( )
hdrs , bodyOffset , err := parseHeaders ( bufio . NewReader ( & moxio . AtReader { R : r } ) )
if err != nil {
return nil , fmt . Errorf ( "%w: %s" , ErrHeaderMalformed , err )
}
// todo: reuse body hashes and possibly verify signatures in parallel. and start the dns lookup immediately. ../rfc/6376:2697
for _ , h := range hdrs {
if h . lkey != "dkim-signature" {
continue
}
sig , verifySig , err := parseSignature ( h . raw , smtputf8 )
if err != nil {
// ../rfc/6376:2503
err := fmt . Errorf ( "parsing DKIM-Signature header: %w" , err )
results = append ( results , Result { StatusPermerror , nil , nil , err } )
continue
}
h , canonHeaderSimple , canonDataSimple , err := checkSignatureParams ( ctx , sig )
if err != nil {
results = append ( results , Result { StatusPermerror , nil , nil , err } )
continue
}
// ../rfc/6376:2560
if err := policy ( sig ) ; err != nil {
err := fmt . Errorf ( "%w: %s" , ErrPolicy , err )
results = append ( results , Result { StatusPolicy , nil , nil , err } )
continue
}
br := bufio . NewReader ( & moxio . AtReader { R : r , Offset : int64 ( bodyOffset ) } )
status , txt , err := verifySignature ( ctx , resolver , sig , h , canonHeaderSimple , canonDataSimple , hdrs , verifySig , br , ignoreTestMode )
results = append ( results , Result { status , sig , txt , err } )
}
return results , nil
}
// check if signature is acceptable.
// Only looks at the signature parameters, not at the DNS record.
func checkSignatureParams ( ctx context . Context , sig * Sig ) ( hash crypto . Hash , canonHeaderSimple , canonBodySimple bool , rerr error ) {
// "From" header is required, ../rfc/6376:2122 ../rfc/6376:2546
var from bool
for _ , h := range sig . SignedHeaders {
if strings . EqualFold ( h , "from" ) {
from = true
break
}
}
if ! from {
return 0 , false , false , fmt . Errorf ( ` %w: required "from" header not signed ` , ErrFrom )
}
// ../rfc/6376:2550
if sig . ExpireTime >= 0 && sig . ExpireTime < timeNow ( ) . Unix ( ) {
return 0 , false , false , fmt . Errorf ( "%w: expiration time %q" , ErrSigExpired , time . Unix ( sig . ExpireTime , 0 ) . Format ( time . RFC3339 ) )
}
// ../rfc/6376:2554
// ../rfc/6376:3284
// Refuse signatures that reach beyond declared scope. We use the existing
// publicsuffix.Lookup to lookup a fake subdomain of the signing domain. If this
// supposed subdomain is actually an organizational domain, the signing domain
// shouldn't be signing for its organizational domain.
subdom := sig . Domain
subdom . ASCII = "x." + subdom . ASCII
if subdom . Unicode != "" {
subdom . Unicode = "x." + subdom . Unicode
}
if orgDom := publicsuffix . Lookup ( ctx , subdom ) ; subdom . ASCII == orgDom . ASCII {
return 0 , false , false , fmt . Errorf ( "%w: %s" , ErrTLD , sig . Domain )
}
h , hok := algHash ( sig . AlgorithmHash )
if ! hok {
return 0 , false , false , fmt . Errorf ( "%w: %q" , ErrHashAlgorithmUnknown , sig . AlgorithmHash )
}
t := strings . SplitN ( sig . Canonicalization , "/" , 2 )
switch strings . ToLower ( t [ 0 ] ) {
case "simple" :
canonHeaderSimple = true
case "relaxed" :
default :
return 0 , false , false , fmt . Errorf ( "%w: header canonicalization %q" , ErrCanonicalizationUnknown , sig . Canonicalization )
}
canon := "simple"
if len ( t ) == 2 {
canon = t [ 1 ]
}
switch strings . ToLower ( canon ) {
case "simple" :
canonBodySimple = true
case "relaxed" :
default :
return 0 , false , false , fmt . Errorf ( "%w: body canonicalization %q" , ErrCanonicalizationUnknown , sig . Canonicalization )
}
// We only recognize query method dns/txt, which is the default. ../rfc/6376:1268
if len ( sig . QueryMethods ) > 0 {
var dnstxt bool
for _ , m := range sig . QueryMethods {
if strings . EqualFold ( m , "dns/txt" ) {
dnstxt = true
break
}
}
if ! dnstxt {
return 0 , false , false , fmt . Errorf ( "%w: need dns/txt" , ErrQueryMethod )
}
}
return h , canonHeaderSimple , canonBodySimple , nil
}
// lookup the public key in the DNS and verify the signature.
func verifySignature ( ctx context . Context , resolver dns . Resolver , sig * Sig , hash crypto . Hash , canonHeaderSimple , canonDataSimple bool , hdrs [ ] header , verifySig [ ] byte , body * bufio . Reader , ignoreTestMode bool ) ( Status , * Record , error ) {
// ../rfc/6376:2604
status , record , _ , err := Lookup ( ctx , resolver , sig . Selector , sig . Domain )
if err != nil {
// todo: for temporary errors, we could pass on information so caller returns a 4.7.5 ecode, ../rfc/6376:2777
return status , nil , err
}
status , err = verifySignatureRecord ( record , sig , hash , canonHeaderSimple , canonDataSimple , hdrs , verifySig , body , ignoreTestMode )
return status , record , err
}
// verify a DKIM signature given the record from dns and signature from the email message.
func verifySignatureRecord ( r * Record , sig * Sig , hash crypto . Hash , canonHeaderSimple , canonDataSimple bool , hdrs [ ] header , verifySig [ ] byte , body * bufio . Reader , ignoreTestMode bool ) ( rstatus Status , rerr error ) {
if ! ignoreTestMode {
// ../rfc/6376:1558
y := false
for _ , f := range r . Flags {
if strings . EqualFold ( f , "y" ) {
y = true
break
}
}
if y {
defer func ( ) {
if rstatus != StatusPass {
rstatus = StatusNone
}
} ( )
}
}
// ../rfc/6376:2639
if len ( r . Hashes ) > 0 {
ok := false
for _ , h := range r . Hashes {
if strings . EqualFold ( h , sig . AlgorithmHash ) {
ok = true
break
}
}
if ! ok {
return StatusPermerror , fmt . Errorf ( "%w: dkim dns record expects one of %q, message uses %q" , ErrHashAlgNotAllowed , strings . Join ( r . Hashes , "," ) , sig . AlgorithmHash )
}
}
// ../rfc/6376:2651
if ! strings . EqualFold ( r . Key , sig . AlgorithmSign ) {
return StatusPermerror , fmt . Errorf ( "%w: dkim dns record requires algorithm %q, message has %q" , ErrSigAlgMismatch , r . Key , sig . AlgorithmSign )
}
// ../rfc/6376:2645
if r . PublicKey == nil {
return StatusPermerror , ErrKeyRevoked
} else if rsaKey , ok := r . PublicKey . ( * rsa . PublicKey ) ; ok && rsaKey . N . BitLen ( ) < 1024 {
// todo: find a reference that supports this.
return StatusPermerror , ErrWeakKey
}
// ../rfc/6376:1541
if ! r . ServiceAllowed ( "email" ) {
return StatusPermerror , ErrKeyNotForEmail
}
for _ , t := range r . Flags {
// ../rfc/6376:1575
// ../rfc/6376:1805
if strings . EqualFold ( t , "s" ) && sig . Identity != nil {
if sig . Identity . Domain . ASCII != sig . Domain . ASCII {
return StatusPermerror , fmt . Errorf ( "%w: i= identity domain %q must match d= domain %q" , ErrDomainIdentityMismatch , sig . Domain . ASCII , sig . Identity . Domain . ASCII )
}
}
}
if sig . Length >= 0 {
// todo future: implement l= parameter in signatures. we don't currently allow this through policy check.
return StatusPermerror , fmt . Errorf ( "l= (length) parameter in signature not yet implemented" )
}
// We first check the signature is with the claimed body hash is valid. Then we
// verify the body hash. In case of invalid signatures, we won't read the entire
// body.
// ../rfc/6376:1700
// ../rfc/6376:2656
dh , err := dataHash ( hash . New ( ) , canonHeaderSimple , sig , hdrs , verifySig )
if err != nil {
// Any error is likely an invalid header field in the message, hence permanent error.
return StatusPermerror , fmt . Errorf ( "calculating data hash: %w" , err )
}
switch k := r . PublicKey . ( type ) {
case * rsa . PublicKey :
if err := rsa . VerifyPKCS1v15 ( k , hash , dh , sig . Signature ) ; err != nil {
return StatusFail , fmt . Errorf ( "%w: rsa verification: %s" , ErrSigVerify , err )
}
case ed25519 . PublicKey :
if ok := ed25519 . Verify ( k , dh , sig . Signature ) ; ! ok {
return StatusFail , fmt . Errorf ( "%w: ed25519 verification" , ErrSigVerify )
}
default :
return StatusPermerror , fmt . Errorf ( "%w: unrecognized signature algorithm %q" , ErrSigAlgorithmUnknown , r . Key )
}
bh , err := bodyHash ( hash . New ( ) , canonDataSimple , body )
if err != nil {
// Any error is likely some internal error, hence temporary error.
return StatusTemperror , fmt . Errorf ( "calculating body hash: %w" , err )
}
if ! bytes . Equal ( sig . BodyHash , bh ) {
return StatusFail , fmt . Errorf ( "%w: signature bodyhash %x != calculated bodyhash %x" , ErrBodyhashMismatch , sig . BodyHash , bh )
}
return StatusPass , nil
}
func algHash ( s string ) ( crypto . Hash , bool ) {
if strings . EqualFold ( s , "sha1" ) {
return crypto . SHA1 , true
} else if strings . EqualFold ( s , "sha256" ) {
return crypto . SHA256 , true
}
return 0 , false
}
// bodyHash calculates the hash over the body.
func bodyHash ( h hash . Hash , canonSimple bool , body * bufio . Reader ) ( [ ] byte , error ) {
// todo: take l= into account. we don't currently allow it for policy reasons.
var crlf = [ ] byte ( "\r\n" )
if canonSimple {
// ../rfc/6376:864, ensure body ends with exactly one trailing crlf.
ncrlf := 0
for {
buf , err := body . ReadBytes ( '\n' )
if len ( buf ) == 0 && err == io . EOF {
break
}
if err != nil && err != io . EOF {
return nil , err
}
hascrlf := bytes . HasSuffix ( buf , crlf )
if hascrlf {
buf = buf [ : len ( buf ) - 2 ]
}
if len ( buf ) > 0 {
for ; ncrlf > 0 ; ncrlf -- {
h . Write ( crlf )
}
h . Write ( buf )
}
if hascrlf {
ncrlf ++
}
}
h . Write ( crlf )
} else {
hb := bufio . NewWriter ( h )
// We go through the body line by line, replacing WSP with a single space and removing whitespace at the end of lines.
// We stash "empty" lines. If they turn out to be at the end of the file, we must drop them.
stash := & bytes . Buffer { }
var line bool // Whether buffer read is for continuation of line.
var prev byte // Previous byte read for line.
linesEmpty := true // Whether stash contains only empty lines and may need to be dropped.
var bodynonempty bool // Whether body is non-empty, for adding missing crlf.
var hascrlf bool // Whether current/last line ends with crlf, for adding missing crlf.
for {
// 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.
buf , err := body . ReadBytes ( '\n' )
if len ( buf ) == 0 && err == io . EOF {
break
}
if err != nil && err != io . EOF {
return nil , err
}
bodynonempty = true
hascrlf = bytes . HasSuffix ( buf , crlf )
if hascrlf {
buf = buf [ : len ( buf ) - 2 ]
// ../rfc/6376:893, "ignore all whitespace at the end of lines".
// 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?
buf = bytes . TrimRight ( buf , " \t" )
}
// Replace one or more WSP to a single SP.
for i , c := range buf {
wsp := c == ' ' || c == '\t'
if ( i >= 0 || line ) && wsp {
if prev == ' ' {
continue
}
prev = ' '
c = ' '
} else {
prev = c
}
if ! wsp {
linesEmpty = false
}
stash . WriteByte ( c )
}
if hascrlf {
stash . Write ( crlf )
}
line = ! hascrlf
if ! linesEmpty {
hb . Write ( stash . Bytes ( ) )
stash . Reset ( )
linesEmpty = true
}
}
// ../rfc/6376:886
// Only for non-empty bodies without trailing crlf do we add the missing crlf.
if bodynonempty && ! hascrlf {
hb . Write ( crlf )
}
hb . Flush ( )
}
return h . Sum ( nil ) , nil
}
func dataHash ( h hash . Hash , canonSimple bool , sig * Sig , hdrs [ ] header , verifySig [ ] byte ) ( [ ] byte , error ) {
headers := ""
revHdrs := map [ string ] [ ] header { }
for _ , h := range hdrs {
revHdrs [ h . lkey ] = append ( [ ] header { h } , revHdrs [ h . lkey ] ... )
}
for _ , key := range sig . SignedHeaders {
lkey := strings . ToLower ( key )
h := revHdrs [ lkey ]
if len ( h ) == 0 {
continue
}
revHdrs [ lkey ] = h [ 1 : ]
s := string ( h [ 0 ] . raw )
if canonSimple {
// ../rfc/6376:823
// Add unmodified.
headers += s
} else {
ch , err := relaxedCanonicalHeaderWithoutCRLF ( s )
if err != nil {
return nil , fmt . Errorf ( "canonicalizing header: %w" , err )
}
headers += ch + "\r\n"
}
}
// ../rfc/6376:2377, canonicalization does not apply to the dkim-signature header.
h . Write ( [ ] byte ( headers ) )
dkimSig := verifySig
if ! canonSimple {
ch , err := relaxedCanonicalHeaderWithoutCRLF ( string ( verifySig ) )
if err != nil {
return nil , fmt . Errorf ( "canonicalizing DKIM-Signature header: %w" , err )
}
dkimSig = [ ] byte ( ch )
}
h . Write ( dkimSig )
return h . Sum ( nil ) , nil
}
// a single header, can be multiline.
func relaxedCanonicalHeaderWithoutCRLF ( s string ) ( string , error ) {
// ../rfc/6376:831
t := strings . SplitN ( s , ":" , 2 )
if len ( t ) != 2 {
return "" , fmt . Errorf ( "%w: invalid header %q" , ErrHeaderMalformed , s )
}
// Unfold, we keep the leading WSP on continuation lines and fix it up below.
v := strings . ReplaceAll ( t [ 1 ] , "\r\n" , "" )
// Replace one or more WSP to a single SP.
var nv [ ] byte
var prev byte
for i , c := range [ ] byte ( v ) {
if i >= 0 && c == ' ' || c == '\t' {
if prev == ' ' {
continue
}
prev = ' '
c = ' '
} else {
prev = c
}
nv = append ( nv , c )
}
ch := strings . ToLower ( strings . TrimRight ( t [ 0 ] , " \t" ) ) + ":" + strings . Trim ( string ( nv ) , " \t" )
return ch , nil
}
type header struct {
key string // Key in original case.
lkey string // Key in lower-case, for canonical case.
value [ ] byte // Literal header value, possibly spanning multiple lines, not modified in any way, including crlf, excluding leading key and colon.
raw [ ] byte // Like value, but including original leading key and colon. Ready for use as simple header canonicalized use.
}
func parseHeaders ( br * bufio . Reader ) ( [ ] header , int , error ) {
var o int
var l [ ] header
var key , lkey string
var value [ ] byte
var raw [ ] byte
for {
line , err := readline ( br )
if err != nil {
return nil , 0 , err
}
o += len ( line )
if bytes . Equal ( line , [ ] byte ( "\r\n" ) ) {
break
}
if line [ 0 ] == ' ' || line [ 0 ] == '\t' {
if len ( l ) == 0 && key == "" {
return nil , 0 , fmt . Errorf ( "malformed message, starts with space/tab" )
}
value = append ( value , line ... )
raw = append ( raw , line ... )
continue
}
if key != "" {
l = append ( l , header { key , lkey , value , raw } )
}
t := bytes . SplitN ( line , [ ] byte ( ":" ) , 2 )
if len ( t ) != 2 {
return nil , 0 , fmt . Errorf ( "malformed message, header without colon" )
}
key = strings . TrimRight ( string ( t [ 0 ] ) , " \t" ) // todo: where is this specified?
// Check for valid characters. ../rfc/5322:1689 ../rfc/6532:193
for _ , c := range key {
if c <= ' ' || c >= 0x7f {
return nil , 0 , fmt . Errorf ( "invalid header field name" )
}
}
if key == "" {
return nil , 0 , fmt . Errorf ( "empty header key" )
}
lkey = strings . ToLower ( key )
value = append ( [ ] byte { } , t [ 1 ] ... )
raw = append ( [ ] byte { } , line ... )
}
if key != "" {
l = append ( l , header { key , lkey , value , raw } )
}
return l , o , nil
}
func readline ( r * bufio . Reader ) ( [ ] byte , error ) {
var buf [ ] byte
for {
line , err := r . ReadBytes ( '\n' )
if err != nil {
return nil , err
}
if bytes . HasSuffix ( line , [ ] byte ( "\r\n" ) ) {
if len ( buf ) == 0 {
return line , nil
}
return append ( buf , line ... ) , nil
}
buf = append ( buf , line ... )
}
}