Okay, here's a working version of the plugin I have.  You can get Mail::DKIM
from here http://jason.long.name/dkimproxy/ along with a program that I used
as a template, dkimproxy, and that works with Postfix.  Rather than puzzling
out how to get that kind of a program to work for qmail I integrated the 
methods into the attached plugin.  I run the plugin with this line in the
plugins file

  dkimsign selector=alecto domains=bittwiddlers.com,bitnerd.com 
keyfile=/etc/ssl/certs/dkim-alecto.private

and have it set up to only sign messages being sent from allowable relay
clients (so I know it's originating here) and being sent from one of the
specified domains.  It seems to work fine with all of the automated test
filters I tried it against.  If anyone finds a problem or fixes anything
please let me know so I can update my version.

My next task will be to finish my dkimcheck plugin which will check the 
DKIM / DomainKey signature on incoming messages

-- 
  Matthew Harrell                          I don't suffer from insanity - 
  Bit Twiddlers, Inc.                       I enjoy every minute of it.
  [EMAIL PROTECTED]     
=head1 NAME

dkimsign -- Compute and insert a DKIM signature into a message

=head1 DESCRIPTION

This plugin will check the message against the specified list of domains 
and DKIM sign it if it's from an address that it's authorized to sign for.

=head1 CONFIG

There are three required parameters for this plugin to work correctly:
the selector name, the domains it can sign for, and the private keyfile.
All other arguments are optional and have sane default values.

=over 4

=item domains=[signing domains]

This parameter defines the comma separated list of domains for which the
plugin will sign messages.

=item keyfile=[/path/to/private.key]

This is the path to the private DKIM key that messages will be signed with.

=item selector=[selector name]

This is the selector name for the key that is signing.

=item method=[simple|nowsp|relaxed|nofws]

Select the canonicalization method.  Currently defaults to "relaxed"

=item type=[dkim|domainkeys]

Whether to do DKIM or DomainKeys signing.  Currently only DKIM is supported
=back

=head1 TODO

Add in the ability to specify a regex for the key name so different keys
can be specified for different domains.

Add in DomainKeys signing (inherent in the DKIM library).

=cut


use strict;
use Mail::DKIM;
use Mail::DKIM::Signer;

# enable support for "pretty" signatures, if available
#  seems to break when using qmail but works for postfix?
#eval "require Mail::DKIM::TextWrap";


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

  $self->{_method} = "relaxed";
  $self->{_type} = "dkim";

  for ( @args ) {
    if ( /^domains=([\.\,a-z0-9A-Z]*)$/ ) {
      $self->{_domains} = $1;
    }
    elsif ( /^keyfile=(\/[\/\-\_\.a-z0-9A-Z]*)$/ ) {
      $self->{_keyfile} = $1;
    }
    elsif ( /^method=(simple|nowsp|relaxed|nofws)$/ ) {
      $self->{_method} = $1;
    }
    elsif ( /^selector=([\.a-z0-9A-Z]*)$/ ) {
      $self->{_selector} = $1;
    }
    elsif ( /^type=(dkim|domainkeys)$/ ) {
      $self->{_type} = $1;
    }
    else {
      $self->log(LOGERROR, "Unrecognized argument '$_' to dkimsign plugin");
      return undef;
    }
  }

  # $self->log ( LOGNOTICE, "dkimsign args: domains: " . $self->{_domains}
  #              . "  keyfile: " . $self->{_keyfile} . "  method: "
  #              . $self->{_method} . "  selector: " . $self->{_selector}
  #              . "  type: " . $self->{_type} );

  unless ( $self->{_domains} ) {
    $self->log ( LOGERROR, "No domains defined" );
    return undef;
  }
  unless ( $self->{_keyfile} ) {
    $self->log ( LOGERROR, "No keyfile defined" );
    return undef;
  }
  unless ( $self->{_selector} ) {
    $self->log ( LOGERROR, "No selector defined" );
    return undef;
  }

  1;
}


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

  # don't bother to continue if we're not allowed to relay for this client
  #
  unless ( $self->qp->connection->relay_client ) {
    return DECLINED;
  }

  my @domains = split ( ",", $self->{_domains} );
  my $address = $transaction->sender->host;

  # ensure that the domain we're sending from is one of the signing domains
  #
  foreach my $domain ( @domains ) {
    # $self->log ( LOGNOTICE, "DKIM: comparing $domain to $address" );

    if ( $domain eq $address ) {

      my $dkim = new Mail::DKIM::Signer (
        Domain   => $address,
        KeyFile  => $self->{_keyfile},
        Method   => $self->{_method},
        Selector => $self->{_selector},
      );

      # take all the headers, reformat them to eliminate cr/lf and push into
      #  dkim.  dkim seems particular about the cr/lf
      #
      my %hdrs = %{ $transaction->header->header_hashref() };

      foreach my $key ( keys %hdrs ) {
        my $val = join ( "", @{$hdrs{$key}} );
        $val =~ s/[\n\r]//g;

        # $self->log ( LOGNOTICE, "Hdr: " . $key . ": " . $val );
        $dkim->PRINT ( $key . ": " . $val . "\x0D\x0A" );
      }

      # 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 ( LOGNOTICE, "Body: " . $line );
        $dkim->PRINT ( $line . "\x0D\x0A" );
      }

      $dkim->CLOSE;
      $self->log ( LOGNOTICE, "dkimsign: " . $dkim->result_detail . "; "
                   . join(", ", $dkim->message_attributes) );

      # add each signature
      #
      foreach my $sig ( $dkim->signatures ) {
        # $self->log ( LOGNOTICE, $sig->as_string );
        $transaction->header->add ( undef, $sig->as_string );
      }

      # eventually we need the capability to do multiple signatures
      return ( DECLINED );
    }
  }

  return ( DECLINED );
}

Reply via email to