Hello Robin and all other QPSMTPD devs,

Here is my latest dkimcheck which I originally took from
http://alecto.bittwiddlers.com/files/qpsmtpd/dkimcheck  and modified to
do the checks and reject if it finds failed signatures and the policy
says that all messages are signed.

This is just a hack and probably has lots of stuff wrong with it but I'm
using it in production on about 6 different servers.

I really like that it rejects mail to my server that actually comes from
spammers saying that it is coming from my server.

Please take it and improve on it and let me know if you do.

A few weeks ago a Juno.com user was getting rejected (I believe that
juno.com is not correctly signing their emails or, more probably, adding
stuff to it after it has been signed) so I added a DKIM host whitelist
today to allow me to override the DKIM results.

   - Thanks
   - David

#! /usr/bin/perl -w

=head1 NAME

dkimcheck -- Check the DKIM / DomainKeys signatures in a message

=head1 DESCRIPTION

If an incoming message has a DKIM signature then this plugin will check
the validity of the message and report the results as a header in the 
mail message.

=head1 CONFIG

None needed right now

=head1 TODO

Verify that checks on DomainKeys are working

Determine if there is a way to individually check signatures.  The DKIM
library wants to return the best result of the available signatures.

Possibly put a timeout in there for the DNS lookups

Add in ability to reject messages that fail the check

=head1 REQUIREMENTS

This module requires the Mail::DKIM module found on CPAN here:

L<http://search.cpan.org/~jaslong/Mail-DKIM-0.26/lib/Mail/DKIM.pm>

=head1 AUTHOR

Written by Matthew Harrell <mharr...@bittwiddlers.com>.

=cut


use strict;
use Mail::DKIM;
use Mail::DKIM::Verifier;


sub register {
  my ($self, $qp, @args) = @_; 

  $self->log(LOGERROR, "Bad parameters for the dkimcheck plugin")
    if @_ % 2;

  %{$self->{_args}} = @args;
}

sub hook_data_post
{
  my ($self, $transaction) = @_;

  # Accept mail if relaying is valid.
  if ( exists( $ENV{ RELAYCLIENT } ) ||
       $self->qp->connection->relay_client() )
  {
    return ( DECLINED, "DKIMCHECK:(0) Mail Accepted: Relay client." );
  }

  # Get the signature count.
  my $dkimcount = $transaction->header->count ( 'DKIM-Signature' );
  my $dkcount = $transaction->header->count ( 'DomainKey-Signature' );

  my $dkim = new Mail::DKIM::Verifier;
  if ( !$dkim )
  {
    $self->log( LOGERROR, "DKIMCHECK:(1) - Could not create 
Mail::DKIM::Verifier" );
    return DECLINED;
  }
  
  # take all the headers, reformat them to eliminate cr/lf and push into
  #  dkim.  dkim seems particular about the cr/lf
  #
  foreach my $line ( split ( /\n/s, $transaction->header->as_string ) )
  {
    $line =~ s/\r?$/\015\012/s;
    # $self->log ( LOGDEBUG, "DKIMCHECK: Hdr: " . $line );
    $dkim->PRINT ( $line );
  }

  # push the body of the message on ensuring the cr/lf are correct
  #
  $transaction->body_resetpos;

  while ( my $line = $transaction->body_getline )
  {
    chomp ( $line );
    $line =~ s/\015$//;

    # $self->log ( LOGDEBUG, "DKIMCHECK: Body: " . $line );
    $dkim->PRINT ( $line . "\x0D\x0A" );
  }

  $dkim->CLOSE;

  my $result = $dkim->result;

  # get the key policy - need to act on this
  #
  my $dns_policy = $dkim->fetch_author_policy;
  my $policy = $dns_policy;
  
  # If we could not fetch a policy then set our local default policy "o=-"
  if ( $dns_policy->is_implied_default_policy )
  {
    $self->log( LOGDEBUG, "DKIMCHECK:(2) NO DNS POLICY FOUND for " .
      $dkim->message_sender->host . " - USING LOCAL POLICY" );

    $policy = Mail::DKIM::DkimPolicy->parse( String => "o=-" );
  }

  my $policy_result = $policy->apply ( $dkim );

  $self->log( LOGDEBUG, "DKIMCHECK:(3) " .
    "DKIM Headers: "      . $dkimcount    .
    "  DK Headers: "      . $dkcount      .
    ", Result: "          . $dkim->result_detail .
    ", Policy: "          . $policy->as_string .
    ", Policy Location: " . ($policy->location ? $policy->location : "undef") .
    ", Policy Result: "   . $policy_result );

  # Deny mail if we find a sign-all policy but no signature.
  if ( ($result eq "none" ||
        $dkim->result_detail =~ /no public key available/ ) &&
       !$dns_policy->is_implied_default_policy && # We found a DNS policy.
       $dns_policy->signall )
  {
    return ( DENY, "DKIM:(4) Mail Denied: No DKIM sig found but sign-all " .
      "policy found for domain " . $dkim->message_sender->host );
  }

  # Accept mail if we found no sig and no policy for domain.
  if ( ($result eq "none" ||
        $dkim->result_detail =~ /no public key available/ ) &&
       $dns_policy->is_implied_default_policy )
  {
    $self->log( LOGDEBUG, "DKIMCHECK:(5) Mail Accepted: No DKIM signature " .
      "and no policy found for domain " . $dkim->message_sender->host );

    add_header( $transaction, $dkim, $policy, $policy_result );

    return ( DECLINED, "DKIM:(6) Mail Accepted: No DKIM signature and " .
      "no policy found for domain " . $dkim->message_sender->host );
  }

  # Accept mail with no signature and neutral policy.
  if ( $result eq "none" && $policy_result eq "neutral" )
  {
    add_header( $transaction, $dkim, $policy, $policy_result );

    return( DECLINED, "DKIM:(7) Mail Accepted: Result: " .
      $dkim->result_detail .  ", Policy: " . $policy_result );
  }

  # Accept mail with good sig and neutral policy.
  if ( $result eq "pass" && $policy_result eq "neutral" )
  {
    add_header( $transaction, $dkim, $policy, $policy_result );

    return( DECLINED, "DKIM:(8) Mail Accepted: Result " . $dkim->result_detail .
      ", Policy: " . $policy_result );
  }

  # Accept mail with signature but no key.
  if ( $result eq "invalid" &&
       $dkim->result_detail =~ /public key: not available/ )
  {
    add_header( $transaction, $dkim, $policy, $policy_result );

    return ( DECLINED, "DKIM:(9) Mail Accepted: Found signature but " .
      "no public key." );
  }

  # Accept mail that doesn't pass but the policy is neutral.
  if ( $result ne "pass" && $policy_result eq "neutral" )
  {
    return ( DECLINED, "DKIM:(10) Result: " . $dkim->result_detail .
      ", Policy: " . $policy_result );
  }

  # Try again later because we got a DNS query timeout.
  if ( $result eq "invalid" && $dkim->result_detail =~ /DNS query timeout/i )
  {
    return( DENYSOFT, "DKIM:(11) Please try again later - " . 
$dkim->result_detail );
  }

  # Accept Whitelist hosts.
  $self->log( LOGERROR, "DKIMCHECK: Whitelist parameter not found." ) unless 
$self->{_args}->{whitelist};
  if ( $self->{_args}->{whitelist} )
  {
    open( WHITELIST, "<" . $self->{_args}->{whitelist} ) || die "Can't open " . 
$self->{_args}->{whitelist} . "\n";
    while ( <WHITELIST> )
    {
      chomp;
      $self->log( LOGWARN, "Comparing received '" . $dkim->message_sender->host 
. "' to whitelist '" . $_ . "'" );
      if ( lc( $dkim->message_sender->host ) eq lc( $_ ) )
      {
        add_header( $transaction, $dkim, $policy, $policy_result );

        return( DECLINED, "DKIM:(12) Accept: From host ". 
$dkim->message_sender->host . " on whitelist." );
      }
    }
    close( WHITELIST ) || die "Can't close WHITELIST file.\n";
  }

  # Deny mail that doesn't pass or is not accepted by the policy.
  if ( $result ne "pass" || $policy_result ne "accept" )
  {
    return ( DENY, "DKIM:(13) Result: " . $result . ", ResultDetail: " . 
$dkim->result_detail .
      ", Policy: " . $policy_result );
  }


  # print the result
  #

  $self->log ( LOGDEBUG, "DKIMCHECK:(14) domain: " . $dkim->signature->domain .
                          ", selector: " . $dkim->signature->selector .
                          ", result: " . $dkim->result_detail .
                          ", policy: " . $policy_result .
                          ", policy_location: " . $policy->location );


  add_header( $transaction, $dkim, $policy, $policy_result );

  return ( DECLINED, "DKIM:(15) Mail Accepted, Result: " . $result .
    ", Policy: " . $policy_result );
}

sub add_header
{
  my ( $transaction, $dkim, $policy, $policy_result ) = @_;

  eval
  {
    $transaction->header->replace (
      "X-DKIM-Authentication: ",
      "domain: " . $dkim->signature->domain .
      ", selector: " . $dkim->signature->selector .
      ", result: " . $dkim->result_detail .
      ", policy: " . $policy_result .
      ", policy_location: ". $policy->location );
  }
}


Reply via email to