You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by mm...@apache.org on 2007/10/02 03:12:12 UTC

svn commit: r581122 - /spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm

Author: mmartinec
Date: Mon Oct  1 18:12:12 2007
New Revision: 581122

URL: http://svn.apache.org/viewvc?rev=581122&view=rev
Log:
revise r580453, accomodating some of the concerns in bug 5662#1 for Plugin::DKIM

Modified:
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm?rev=581122&r1=581121&r2=581122&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm Mon Oct  1 18:12:12 2007
@@ -42,53 +42,7 @@
   http://jason.long.name/dkimproxy/
   http://tools.ietf.org/rfc/rfc4871.txt
   http://tools.ietf.org/rfc/rfc4870.txt
-  http://www.ietf.org/internet-drafts/draft-ietf-dkim-ssp-01.txt
-  http://www.ietf.org/internet-drafts/draft-ietf-dkim-overview-05.txt
-
-=head1 A BRIEF INTRODUCTION TO TERMINOLOGY
-
-B<Originator Address> is the author's e-mail address in a "From:" header field.
-
-A message may carry one or more B<signatures> (signature header fields),
-each signature carries an B<identity> (the i= tag) telling who provided it
-and how it can be verified.
-
-Only B<valid signatures> matter (i.e. verified signatures), all invalid
-signatures can be and MUST be ignored (making a distinction here can provide
-advantage to malicious senders or spam senders, as faking an invalid signature
-is trivial).
-
-A valid signature (one or more) whose identity matches I<Originator Address>
-is called B<Originator Signature> (or sometimes a I<First-Party Signature>),
-and is normally acceptable to recipients unconditionally (that doesn't say
-anything about the merits of a message, it just tells the message is coming
-from where it claims to be coming).
-
-A I<valid signature> which is I<not an Originator Signature> is called a
-B<Third-Party Signature> (e.g. supplied by a mailing list or re-mailer).
-
-It is up to a recipient (the verifier) to decide which I<Third-Party
-Signatures> are acceptable to him (e.g. those provided by a reputable
-mailing list server with good anti-spam measures) and which are not.
-Such signatures are called B<Verifier Acceptable Third-Party Signatures>.
-
-There exists a mechanism called B<DKIM Sender Signing Practices> (SSP),
-by which an I<Originator> (i.e. a mail author or his sending mailer) can
-tell recipients (verifiers) about his signing practices (previously called
-signing policy). An Internet Draft is approaching ratification stage,
-but is not there yet. This plugin does not yet fully implement it.
-
-Having a proof a message is coming from where it claims to be coming
-is an essential stone in anti-phishing and anti-spam protection, but
-is not sufficient by itself. Some B<reputation scheme> is needed, a
-community-based reputation scheme would be useful, but is not available
-for the time being. Currently it is up to verifiers to organize it for
-themselves, e.g. in a form of I<DKIM-based whitelisting>, as provided by
-this plugin. The mechanism allows to whitelist an I<Originator Signature>
-as well as selected I<Third-Party Signatures>.
-
-For details please consult RFC 4871, draft-ietf-dkim-ssp-01 (or later)
-and draft-ietf-dkim-overview-05 (or later).
+  http://ietf.org/html.charters/dkim-charter.html
 
 =cut
 
@@ -146,26 +100,17 @@
 Use this to supplement the whitelist_from addresses with a check to make sure
 the message with a given From: author's address (originator address) carries
 a valid Domain Keys Identified Mail (DKIM) signature by a verifier-acceptable
-signing-identity (the i= tag). Signature verification is based on a signing
-identity's DKIM public key, fetched by a DNS lookup from a domain specified
-in a d= tag of a signature.
-
-In order to support multiple optional verifier-acceptable signing identities
-(e.g. signatures supplied by mailing lists), only one whitelist entry
-is allowed per line, exactly like C<whitelist_from_rcvd>. Multiple
-C<whitelist_from_dkim> lines are allowed. File-glob style meta characters
-are allowed for the From: address, just like with C<whitelist_from_rcvd>.
-
-If no signing identity (second parameter) is specified, the only acceptable
-signature will be an originator signature (not a third-party signature).
-An originator signature is a signature where the signing identity matches
-the originator address (i.e. the address in a From header field).  If the
-signing identity does not include a localpart, then only the domains must
-match; otherwise, the two addresses must be identical. Note that there is
-no subdomain stripping magic, a match on domain must be exact.
+signing-identity (the i= tag).
 
-The originator address is obtained from the "From:" header field, which
-should be in a signed part of the message.
+Only one whitelist entry is allowed per line, as in C<whitelist_from_rcvd>.
+Multiple C<whitelist_from_dkim> lines are allowed. File-glob style
+meta characters are allowed for the From: address (the first parameter),
+just like with C<whitelist_from_rcvd>.
+
+If no signing identity parameter is specified, the only acceptable signature
+will be an originator signature (not a third-party signature). An originator
+signature is a signature where the signing identity of a signature matches
+the originator address (i.e. the address in a From header field).
 
 Since this whitelist requires a DKIM check to be made, network tests must
 be enabled.
@@ -174,18 +119,15 @@
 
   whitelist_from_dkim joe@example.com
   whitelist_from_dkim *@corp.example.com
-  whitelist_from_dkim *@gmail.com
+  whitelist_from_dkim *@*.example.com
 
-Examples of whitelisting based on an third-party signatures:
+Examples of whitelisting based on third-party signatures:
 
   whitelist_from_dkim rick@example.net     richard@example.net
   whitelist_from_dkim rick@sub.example.net example.net
   whitelist_from_dkim jane@example.net     example.org
-  whitelist_from_dkim *@*                  spamassassin.apache.org
-  whitelist_from_dkim *@*                  postfix.org
-
-(the last two examples illustrate singing by mailing lists, although the
-featured mailing lists are not currently re-signing mailing list traffic)
+  whitelist_from_dkim *@info.example.com   example.com
+  whitelist_from_dkim *@*                  remailer.example.com
 
 =item def_whitelist_from_dkim originator@example.com [signing-identity]
 
@@ -207,11 +149,7 @@
         return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
       my $address = $1;
-      my $identity = '';  # when empty only originator signature is acceptable
-      if (defined $2) {   # explicit additional acceptable signing identity
-        $identity = $2;
-        $identity = '@' . $identity  if $identity !~ /\@/;
-      }
+      my $identity = defined $2 ? $2 : ''; # empty implies originator signature
       $self->{parser}->add_to_addrlist_rcvd('whitelist_from_dkim',
                                             $address, $identity);
     }
@@ -229,11 +167,7 @@
         return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
       my $address = $1;
-      my $identity = '';  # when empty only originator signature is acceptable
-      if (defined $2) {   # explicit additional acceptable signing identity
-        $identity = $2;
-        $identity = '@' . $identity  if $identity !~ /\@/;
-      }
+      my $identity = defined $2 ? $2 : ''; # empty implies originator signature
       $self->{parser}->add_to_addrlist_rcvd('def_whitelist_from_dkim',
                                             $address, $identity);
     }
@@ -361,8 +295,8 @@
 
     $scan->{dkim_address} = !$message->message_originator ? ''
                               : $message->message_originator->address();
-    dbg("dkim: originator address: %s",
-        $scan->{dkim_address} ? $scan->{dkim_address} : 'none');
+    dbg("dkim: originator address: ".
+        ($scan->{dkim_address} ? $scan->{dkim_address} : 'none'));
 
     $scan->{dkim_identity} = '';
     if ($message->signature) {
@@ -371,17 +305,20 @@
       #     OPTIONAL, default is an empty local-part followed by an "@"
       #     followed by the domain from the "d=" tag).
       $scan->{dkim_identity} = $message->signature->identity();
+      dbg("dkim: signing identity: ".$scan->{dkim_identity}.
+          ", signing domain: ".$message->signature->domain());
       if ($scan->{dkim_identity} eq '') {
         $scan->{dkim_identity} = '@' . $message->signature->domain();
+      } elsif ($scan->{dkim_identity} !~ /\@/) {
+        $scan->{dkim_identity} = '@' . $scan->{dkim_identity};
       }
-      dbg("dkim: signing identity: ".$scan->{dkim_identity});
     }
 
     my $result = $message->result();
     my $detail = $message->result_detail();
     # let the result stand out more clearly in the log, use uppercase
-    dbg("dkim: signature verification result: %s",
-        $detail eq 'none' ? $detail : uc $detail);
+    dbg("dkim: signature verification result: ".
+        ($detail eq 'none' ? $detail : uc $detail));
 
     # extract the actual lookup results
     if ($result eq 'pass') {
@@ -487,18 +424,21 @@
   return unless $scan->is_dns_available();
 
   # trigger a DKIM check so we can get address/identity info
-  unless ($self->check_dkim_verified($scan)) {
-    return;
-  }
+  my $verified = $self->check_dkim_verified($scan);
+
+  return unless $verified || would_log("dbg","dkim");
+  # continue if verification succeeded or we want the debug info
 
   unless ($scan->{dkim_address}) {
     dbg("dkim: %swhitelist_from_dkim: could not find originator address",
         $default ? "def_" : "");
     return;
   }
-  unless ($scan->{dkim_identity}) {
-    dbg("dkim: %swhitelist_from_dkim: could not find signing identity",
-        $default ? "def_" : "");
+
+  my $identity = $scan->{dkim_identity};
+  unless ($identity) {
+  # (useless double debug line, we already reported if there is no signature)
+  # dbg("dkim: %swhitelist_from_dkim: no signature", $default ? "def_" : "");
     return;
   }
 
@@ -523,62 +463,57 @@
     }
   }
 
+  # prepare summary info string to be used for logging
+  my $info = $identity eq '' ? 'no' : $verified ? 'verified' : 'failed';
+  my $originator_matching_part = $scan->{dkim_address};  # address in 'From'
+  if ($identity =~ /^\@/) {  # empty localpart in signing identity
+    local($1);
+    $originator_matching_part =~ s/^.*?(\@[^\@]*)?$/$1/s;  # strip localpart
+  }
+  $info .= lc $identity eq lc $originator_matching_part ? ' originator'
+                                                        : ' third-party';
+  $info .= " signature by id $identity, originator $scan->{dkim_address}";
+
   # if the message doesn't pass DKIM validation, it can't pass DKIM whitelist
-  if ($default) {  # DEF_DKIM_WHITELIST_FROM
+
+  if ($default) {
     if ($scan->{def_dkim_whitelist_from}) {
-      if ($self->check_dkim_verified($scan)) {  # double-check just in case
-        dbg("dkim: originator %s, signing identity %s, found in ".
-            "def_whitelist_from_dkim and passed DKIM verification",
-          $scan->{dkim_address}, $scan->{dkim_identity});
-      } else {
-        $scan->{def_dkim_whitelist_from} = 0;
-      }
+      dbg("dkim: $info, found in DEF_WHITELIST_FROM_DKIM" .
+          ($verified ? '' : ' but ignored') );
+      if (!$verified) { $scan->{def_dkim_whitelist_from} = 0 }
+    } else {
+      dbg("dkim: $info, not in DEF_WHITELIST_FROM_DKIM");
     }
-#   else {
-#     dbg("dkim: originator %s, signing identity %s, ".
-#         "not in def_whitelist_from_dkim",
-#         $scan->{dkim_address}, $scan->{dkim_identity});
-#   }
-  } else {  # DKIM_WHITELIST_FROM
+  } else {
     if ($scan->{dkim_whitelist_from}) {
-      if ($self->check_dkim_verified($scan)) {  # double-check just in case
-        dbg("dkim: originator %s, signing identity %s, found in ".
-            "whitelist_from_dkim and passed DKIM verification",
-            $scan->{dkim_address}, $scan->{dkim_identity});
-      } else {
-        $scan->{dkim_whitelist_from} = 0;
-      }
+      dbg("dkim: $info, found in WHITELIST_FROM_DKIM" .
+          ($verified ? '' : ' but ignored') );
+      if (!$verified) { $scan->{dkim_whitelist_from} = 0 }
+    } else {
+      dbg("dkim: $info, not in WHITELIST_FROM_DKIM");
     }
-#   else {
-#     dbg("dkim: originator %s, signing identity %s, ".
-#         "not in whitelist_from_dkim",
-#         $scan->{dkim_address}, $scan->{dkim_identity});
-#   }
   }
 }
 
-# check for an originator signature(s), as well as for additional
-# verifier-acceptable signatures if provided in a config as a second
-# parameter on dkim whitelist entries
+# check for verifier-acceptable signatures; an empty (or undefined) signing
+# identity in a whitelist implies checking for an originator signature
 #
 sub _wlcheck_acceptable_signature {
   my ($self, $scan, $wl) = @_;
   foreach my $white_addr (keys %{$scan->{conf}->{$wl}}) {
     my $re = qr/$scan->{conf}->{$wl}->{$white_addr}{re}/i;
-    # check for the originator signature is implied
-    $self->_wlcheck_one($scan, $wl, $white_addr, undef, $re) and return 1;
-    # walk through all additional verifier-acceptable signing identities
-    foreach my $acceptable_identity
-            (@{$scan->{conf}->{$wl}->{$white_addr}{domain}}) {
-      next if !defined $acceptable_identity || $acceptable_identity eq '';
-      $self->_wlcheck_one($scan, $wl, $white_addr, $acceptable_identity, $re)
-        and return 1;
+    if ($scan->{dkim_address} =~ $re) {
+      foreach my $acceptable_identity
+              (@{$scan->{conf}->{$wl}->{$white_addr}{domain}}) {
+        $self->_wlcheck_one($scan, $wl, $white_addr, $acceptable_identity, $re)
+          and return 1;
+      }
     }
   }
   return 0;
 }
 
-# use a traditional whitelist_from-style addrlist, the only acceptable DKIM
+# use a traditional whitelist_from -style addrlist, the only acceptable DKIM
 # signature is an Originator Signature.  Note: don't pre-parse and store the
 # domains; that's inefficient memory-wise and only saves one m//
 #
@@ -600,48 +535,52 @@
   # When $acceptable_identity is undef or an empty string it implies an
   # originator signature check.
 
-  my $originator = $scan->{dkim_address};
-  if ($originator =~ $re) {  # originator address does match a whitelist entry
+  my $matches = 0;
+  my $originator = $scan->{dkim_address};  # address in a 'From' header field
+  my $identity = $scan->{dkim_identity};   # TODO: support multiple signatures
+  if ($originator =~ $re) {
+    # originator address does match a whitelist entry (or we are debugging),
     # but does it carry a signature of a verifier-acceptable signing identity?
-    my $identity = $scan->{dkim_identity};  # TODO: support multiple signatures
 
-    local($1);
+    # An "Originator Signature" is any Valid Signature where the signing
+    # identity matches the Originator Address. If the signing identity
+    # does not include a localpart, then only the domains must match;
+    # otherwise, the two addresses must be identical.
+
+    local($1,$2);
+    my $originator_matching_part = $originator;
+    if ($identity =~ /^\@/) {  # empty localpart in signing identity
+      $originator_matching_part =~ s/^.*?(\@[^\@]*)?$/$1/s;  # strip localpart
+    }
     if (!defined $acceptable_identity || $acceptable_identity eq '') {
       # checking for originator signature
-      #
-      # An "Originator Signature" is any Valid Signature where the signing
-      # identity matches the Originator Address. If the signing identity
-      # does not include a localpart, then only the domains must match;
-      # otherwise, the two addresses must be identical.
-      #
-      my $originator_matching_part = $originator;
-      if ($identity =~ /^\@/) {  # no localpart in signing identity
-        $originator_matching_part =~ s/^.*?(\@[^\@]*)?$/$1/s; # strip localpart
-      }
-      if (lc($originator_matching_part) eq lc($identity)) {
-        dbg("dkim: originator signature from %s, signing identity %s, ".
-            "matches %s %s", $originator, $identity, $wl, $re);
-        return 1;
-      }
-    }
-
-    else {
-      if ($acceptable_identity !~ /\@/) {  # ensure domain part, possibly empty
+      $matches = 1  if lc $identity eq lc $originator_matching_part;
+    } else {  # checking for verifier-acceptable signature
+      if ($acceptable_identity !~ /\@/) {
         $acceptable_identity = '@' . $acceptable_identity;
       }
-      my $identity_matching_part = $identity;
-      if ($acceptable_identity =~ /^\@/) {  # no localpart, just domain?
-        $identity_matching_part =~ s/^.*?(\@[^\@]*)?\z/$1/s;  # strip localpart
-      }
-      if (lc($identity_matching_part) eq lc($acceptable_identity)) {
-        dbg("dkim: originator %s, signing identity %s, ".
-            "verifier-acceptable (%s), matches %s %s",
-            $originator, $identity, $acceptable_identity, $wl, $re);
-        return 1;
-      }
+      # split into local part and domain
+      $identity            =~ /^ (.*?) \@ ([^\@]*) $/xs;
+      my($actual_id_mbx, $actual_id_dom) = ($1,$2);
+      $acceptable_identity =~ /^ (.*?) \@ ([^\@]*) $/xs;
+      my($accept_id_mbx, $accept_id_dom) = ($1,$2);
+
+      # let's take a liberty and compare local parts case-insensitively
+      if ($accept_id_mbx ne '') {  # local part exists, full id must match
+        $matches = 1  if lc $identity eq lc $acceptable_identity;
+      } else {  # any local part in signing identity is acceptable
+                # as long as domain matches or is a subdomain
+        $matches = 1  if $actual_id_dom =~ /(^|\.)\Q$accept_id_dom\Q/i;
+      }
+    }
+    if ($matches && would_log("dbg","dkim")) {
+      my $verified = $self->check_dkim_verified($scan);
+      my $info = $identity eq '' ? 'no' : $verified ? 'verified' : 'failed';
+      dbg("dkim: $info signature by id $identity, originator $originator ".
+          " matches $wl $re");
     }
   }
-  return 0;
+  return $matches;
 }
 
 1;