Linuxdoc Linux Questions
Click here to ask our community of linux experts!
Custom Search

A.11. Adding Envelope Sender Signatures

Here we implement Envelope Sender Signature in our outgoing mail, and check for these signatures before accepting incoming "bounces" (i.e. mail with no envelope sender).

The envelope sender address of outgoing mails from your host will be modified as follows:

sender=recipient=recipient.domain=hash@sender.domain

However, because this scheme may produce unintended consequences (e.g. in the case of mailing list servers), we make it optional for your users. We sign the envelope sender address of outgoing mail only if we find a file named ".return-path-sign" in the sender's home directory, and only if the domain we are sending to is matched in that file. If the file exists, but is empty, all domains match.

Similarly, we only require the recipient address to be signed in incoming "bounce" messages (i.e. messages with no envelope sender) if the same file exists in recipient's home directory. Users can exempt specific hosts from this check via their user specific whitelist, as described in Exempting Forwarded Mail.

Also, because this scheme involves tweaking with routers and transports in addition to ACLs, we do not include it in the Final ACLs to follow. If you are able to follow the instructions pertaining to those sections, you should also be able to add the ACL section as described here.

A.11.1. Create a Transport to Sign the Sender Address

First we create an Exim transport that will be used to sign the envelope sender for remote deliveries:


remote_smtp_signed:
  debug_print    = "T: remote_smtp_signed for $local_part@$domain"
  driver         = smtp
  max_rcpt       = 1
  return_path    = $sender_address_local_part=$local_part=$domain=\
                   ${hash_8:${hmac{md5}{SECRET}{${lc:\
                     $sender_address_local_part=$local_part=$domain}}}}\
                   @$sender_address_domain

The "local part" of the sender address now consists of the following components, separated by equal signs ("="):

  • the sender's username, i.e. the original local part,

  • the local part of the recipient address,

  • the domain part of the recipient address,

  • a string unique to this sender/recipient combination, generated by:

    • encrypting the three prior components of the rewritten sender address, using Exim's ${hmac{md5}...} function along with the SECRET we declared in the main section, [1]

    • hashing the result into 8 lowercase letters, using Exim's ${hash...} function.

If you need authentication for deliveries to "smarthosts", add an appropriate hosts_try_auth line here as well. (Take it from your existing smarthost transport).

A.11.2. Create a New Router for Remote Deliveries

Add a new router prior to the existing router(s) that currently handles your outgoing mail. This router will use the transport above for remote deliveries, but only if the file ".return-path-sign" exists in the sender's home directory, and if the recipient's domain is matched in that file. For instance, if you send mail directly over the internet to the final destination:


# Sign the envelope sender address (return path) for deliveries to
# remote domains if the sender's home directory contains the file
# ".return-path-sign", and if the remote domain is matched in that
# file. If the file exists, but is empty, the envelope sender
# address is always signed.  
#
dnslookup_signed:
  debug_print   = "R: dnslookup_signed for $local_part@$domain"
  driver        = dnslookup
  transport     = remote_smtp_signed
  senders       = ! : *
  domains       = ! +local_domains : !+relay_to_domains : \
             ${if exists {/home/$sender_address_local_part/.return-path-sign}\
                         {/home/$sender_address_local_part/.return-path-sign}\
                         {!*}}
  no_more

Or if you use a smarthost:


# Sign the envelope sender address (return path) for deliveries to
# remote domains if the sender's home directory contains the file
# ".return-path-sign", and if the remote domain is matched in that
# file. If the file exists, but is empty, the envelope sender
# address is always signed.
#
smarthost_signed:
  debug_print   = "R: smarthost_signed for $local_part@$domain"
  driver        = manualroute
  transport     = remote_smtp_signed
  senders       = ! : *
  route_list    = * smarthost.address
  host_find_failed = defer
  domains       = ! +local_domains : !+relay_to_domains : \
             ${if exists {/home/$sender_address_local_part/.return-path-sign}\
                         {/home/$sender_address_local_part/.return-path-sign}\
                         {!*}}
  no_more  

Add other options as you see fit (e.g. same_domain_copy_routing = yes), perhaps modelled after your existing routers.

Note that we do not use this router for mails with no envelope sender address - we wouldn't want to tamper with those! [2]

A.11.3. Create New Redirect Router for Local Deliveries

Next, you need to tell Exim that incoming recipient addresses that match the format above should be delivered to the mailbox identified by the portion before the first equal ("=") sign. For this purpose, you want to insert a redirect router early in the routers section of your configuration file - before any other routers pertaining to local deliveries (such as a system alias router):


hashed_local:
  debug_print       = "R: hashed_local for $local_part@$domain"
  driver            = redirect
  domains           = +local_domains
  local_part_suffix = =*
  data              = $local_part@$domain

Recipient addresses that contain a equal sign are rewritten such that the portion of the local part that follows the equal sign are stripped off. Then all routers are processed again.

A.11.4. ACL Signature Check

The final part of this scheme is to tell Exim that mails delivered to valid recipient addresses with this signature should always be accepted, and that other messages with a NULL envelope sender should be rejected if the recipient has opted in to this scheme. No greylisting should be done in either case.

The following snippet should be placed in acl_rcpt_to, prior to any SPF checks, greylisting, and/or the final accept statement:


  # Accept the recipient addresss if it contains our own signature.
  # This means this is a response (DSN, sender callout verification...)
  # to a message that was previously sent from here.
  #
  accept
    domains     = +local_domains
    condition   = ${if and {{match{${lc:$local_part}}{^(.*)=(.*)}}\
                            {eq{${hash_8:${hmac{md5}{SECRET}{$1}}}}{$2}}}\
                           {true}{false}}

  # Otherwise, if this message claims to be a bounce (i.e. if there
  # is no envelope sender), but if the receiver has elected to use
  # and check against envelope sender signatures, reject it.
  #
  deny
    message     = This address does not match a valid, signed \
                  return path from here.\n\
                  You are responding to a forged sender address.
    log_message = bogus bounce.
    senders     = : postmaster@*
    domains     = +local_domains
    set acl_m9  = /home/${extract{1}{=}{${lc:$local_part}}}/.return-path-sign
    condition   = ${if exists {$acl_m9}{true}}


You will have an issue when sending mail to hosts that perform callout verification on addresses in the message header, such as the one provided in the From: field of your outgoing mail. The deny statement here will effectively give a negative response to such a verification attempt.

For that reason, you may want to convert the last deny statement into a warn statement, store the rejection message in $acl_m0, and perform the actual rejection after the DATA command, in a fashion similar to previously described:


  # Otherwise, if this message claims to be a bounce (i.e. if there
  # is no envelope sender), but if the receiver has elected to use
  # and check against envelope sender signatures, store a reject
  # message in $acl_m0, and a log message in $acl_m1.  We will later
  # use these to reject the mail.  In the mean time, their presence 
  # indicate that we should keep stalling the sender.
  #
  warn
    senders     = : postmaster@*
    domains     = +local_domains
    set acl_m9  = /home/${extract{1}{=}{${lc:$local_part}}}/.return-path-sign
    condition   = ${if exists {$acl_m9}{true}}
    set acl_m0  = The recipient address <$local_part@$domain> does not \
                  match a valid, signed return path from here.\n\
                  You are responding to a forged sender address.
    set acl_m1  = bogus bounce for <$local_part@$domain>.

Also, even if the recipient has chosen to use envelope sender signatures in their outgoing mail, they may want to exempt specific hosts from having to provide this signature in incoming mail, even if the mail has no envelope sender address. This may be required for specific mailing list servers, see the discussion on Envelope Sender Signature for details.

Notes

[1]

If you think this is an overkill, would I tend to agree on the surface. In previous versions of this document, I simply used ${hash_8:SECRET=....} to generate the last component of the signature. However, with this it would be technically possible, with a bit of insight into Exim's ${hash...} function and some samples of your outgoing mail sent to different recipients, to forge the signature. Matthew Byng-Maddic notes: What you're writing is a document that you expect many people to just copy. Given that, kerchoff's principle starts applying, and all of your secrecy should be in the key. If the key can be reversed out, as seems likely with a few return paths, then the spammer kan once again start emitting valid return-paths from that domain, and you're back to where you started. [...] Better, IMO, to have it being strong from the start.

[2]

In the examples above, the senders condition is actually redundant, since the file /home//.return-path-sign is not likely to exist. However, we make the condition explicit for clarity.