[Mimedefang] PGP automation?

Paul Murphy Paul.Murphy at argentadiscovery.com
Fri Aug 3 04:53:07 EDT 2007


Gary,

I did something similar to this in a previous position, using GnuPG to support inspection of PGP messages as they passed through MD.  The code checked that all messages to some domains were encrypted, that there were no plain text parts in the message, and that any encrypted message also included the corporate key so that we could recover the contents if required.

The MD server had a copy of the public keys for all staff, and the private key for the company.  The pass phrase for this is the weak link - it had to be coded into the MD filter, or read from a file.  The requirement was that anyone sending PGP messages had to have been authorised to do so, had to use the corporate key in addition to their own and recipient keys, and had to encrypt 100% of the message, since they had a habit of sending messages which said things like "See attached encrypted file, which shows details of how our new project X is going, and especially how our new product Y has great results".  Users, eh?  Gotta love em...

I also ran into some problems where occasionally messages would cause the GnuPG support modules to get a bit confused - the decrypt process would take 100% of the CPU, and fail to die even after the MD slave handling the message had timed out and been killed.  The sending system then retried the same message, with the same result, so even my 4-way server could be crippled by a single message which hit this bug.

There are at least 3 Perl modules which try to interface to PGP, so its up to you which to use - I used Mail::GPG because it had the facilities I wanted at the time, but things have moved on since then, and you'll probably want to re-examine them and use whatever seems most current/popular.   Especially as the version I used had the problem noted above with some messages.

Attached are fragments of my functions which handled this - I defined domains where encryption was mandatory (plus exceptions) in hashes, which were then referenced using the sender and recipient addresses or domains as keys.  If I was doing it again now, I'd probably implement this differently, but it worked.

The tricky bit is implementing a policy which works.  I had all sorts of exceptions to get around the bug mentioned above (e.g. .PGP files assumed to be encrypted since they caused the problem), plus because all messages had to be fully encrypted, how do you handle automatically-added disclaimers, null parts, or messages where the body is "see encrypted attachment", I had to jump through hoops to make sure that the filter still worked, and didn't block trivial cases.

For a case where you want seamless encryption "on the fly", the easiest approach is to use specific keys for each domain, and have the private key on the MD server.  The filter then detects that messages are to that domain, and then for each message part which is not a container (multipart/alternative for example - any part where the MIME entity has no body), you need to extract the contents, encrypt them, add them back to the message as a new part, and remove the original part.  In theory this is easy - in practice the modern versions of PGP instead support whole-message encryption (including attachments) which then builds a specific MIME structure, which you'd have to replicate.

Its an interesting project, but not one I'd like to undertake myself, having been through some of the pain of doing parts of it before...

Paul.
-- 

-------------------------------------------------------
Paul Murphy
Head of I.T.
Argenta Discovery
Tel. 01279 645 554
Fax. 01279 645 646



_______________________________________________________________________
Argenta Discovery Ltd, 8-9 Spire Green Centre, Harlow, Essex, CM19 5TR
Registered in England No. 3671653
_______________________________________________________________________ 

-------------- next part --------------
sub filter ($$$$) {
    my($entity, $fname, $ext, $type) = @_;

    return if message_rejected(); # Avoid unnecessary work

    my $part_head = $entity->head;
    $entity->sync_headers(Length=>'COMPUTE');
    my $length = $part_head->get('Content-length');

    # Work out whether the recipient domain needs encryption
    my %domains;
    my @domtmp;
    my $rcptdom,$senddom;
    my @domlist;
    my $domcount= -1; # start at -1 to allow for sender addition to list
    my $enccount=0;
    my $msgencrypted=0;

    # Count the number of distinct domains, and number requiring encryption
    my @recs;
    push @recs, $Sender;
    foreach my $recip (@Recipients)
      {
      push @recs, $recip;
      }
    my $cleansender=$Sender;
    $cleansender=~ s/<//g;
    $cleansender=~ s/>//g;
    @domtmp = split /@/,$cleansender;
    $senddom=$domtmp[1];
    $senddom=~ s/>//;
    foreach my $recip ( @recs )
      {
      my $cleanrecip=$recip;
      $cleanrecip=~ s/<//g;
      $cleanrecip=~ s/>//g;
      @domtmp = split /@/,$cleanrecip;
      $rcptdom=$domtmp[1];
      $rcptdom=~ s/>//;
      if (! exists $domains{$rcptdom} )
	{
	$domains{$rcptdom}=1;
	$domcount++;
        if ( exists $EncryptionRequired{$rcptdom})
	  {
	  md_syslog('debug',"Exception check - $cleansender -> $cleanrecip");
	  if (! exists $EncryptionException{$cleansender} && ! exists
		       $EncryptionException{$cleanrecip})
	    {
	    $enccount++;
	    push @domlist, $rcptdom;
	    md_syslog('debug',"Domain $rcptdom requires encryption");
	    }
	  else
	    {
	    md_syslog('debug',"Encryption exception detected - $cleansender -> $cleanrecip");
	    }
	  }
	}
      }
    md_syslog('info',"Att-check: MsgID=$MsgID, Name=$fname, Type=$type, size=$length,sender=$Sender, Recips = @Recipients, doms=$domcount, encs=$enccount, lastdom=$rcptdom");
    if ( ($enccount != $domcount ) && ( $enccount != 0 ) )
      {
      md_syslog('info',"Mixing encrypted and un-encrypted domains, sender=$Sender, Recips = @Recipients");
      action_notify_administrator("Unencrypted mail from $Sender to mixed domains including mandatory encrypted domain, recips=@Recipients");
      action_quarantine_entire_message("Sending unencrypted messages to multiple addresses including addresses which require encryption not allowed - see http://pgp.ionixpharma.com/policy.htm for details.");
      action_bounce("    ***   Sending unencrypted messages to multiple addresses including addresses which require encryption not allowed - see http://pgp.ionixpharma.com/policy.htm for details.  Please also ensure that you DO NOT send mail in HTML format.");
      }

    # Detect encrypted messages 
    # S/MIME messages look like: 
		#type=application/pkcs7-mime
		#extension=.p7m, 
		#file=smime.p7m
    $msgencrypted=0;
    if ( (lc($type) eq "application/pkcs7-mime" ) || ($ext eq ".p7m") )
      {
      md_syslog('info',"S/MIME e-mail! - type=$type, extension=$ext");
      $msgencrypted=1;
      md_graphdefang_log('S/MIME');
      }
    # refuse ASCII armored attachments of over 2Mb
    if ( ($ext =~ /asc/i ) && ( $length > 2*1024*1024) )
      {
      md_syslog('debug',"PGP with large ASC attachment - message bounced, size=$length, $MsgID, $Sender, @Recipients\n");
      action_notify_administrator("Bad PGP attachment - ASCII, over 2Mb ($length)\nSender: $Sender\nRecipients: @Recipients\nSubject: $Subject\n");
      action_quarantine_entire_message("Bad PGP ASCII attachment, length=$length");
      md_graphdefang_log('PGP_ASCII');
      return action_bounce("    ***   ASCII armored PGP attachment refused.  Please send all attachments as .PGP files, NOT as .ASC, as this cannot be guaranteed to work.  Please also ensure that you DO NOT send mail in HTML format.");
      }

    $pgplevel=pgp_check($entity,$fname,$type);
    if ( $pgplevel > 0 )
      {
      # $msgencrypted=1;	# temporary fix for testing - remove when live
      md_syslog('debug',"PGP LEVEL INADEQUATE - message bounced, $MsgID, $Sender, @Recipients\n");
      action_notify_administrator("Inadequate PGP key list detected\nSender: $Sender\nRecipients: @Recipients\nSubject: $Subject\n");
      action_quarantine_entire_message("Inadequate PGP key list - corporate key not found, or message could not be decrypted");
      action_bounce("    ***   All encrypted messages must be encrypted to the Ionix Corporate key as well as all recipients.  See http://pgp.ionixpharma.com/policy.htm for details.  Please also ensure that you DO NOT send mail in HTML format.");
      md_graphdefang_log('PGP_keymissing');
      }
    elsif ($pgplevel == 0 )
      {
      md_syslog('debug',"PGP detected, encryption OK - message will be delivered, $MsgID, $Sender, @Recipients\n");
      $msgencrypted=1;
      md_graphdefang_log('PGP_body');
      }
    elsif ($pgplevel == -1 )
      {
      # md_syslog('debug',"PGP - too small to be a problem - message will be delivered, $MsgID, $Sender, @Recipients\n");
      $msgencrypted=1;
      }
    elsif ($pgplevel == -2 )
      {
      md_syslog('debug',"PGP error - partially encrypted, $MsgID, $Sender, @Recipients\n");
      $msgencrypted=0;
      }
      # else not encrypted
    elsif ($pgplevel == -3 )
      {
      md_syslog('debug',"PGP - $fname ($type) Not encrypted, $MsgID, $Sender, @Recipients\n");
      $msgencrypted=0;
      }

    if ( $msgencrypted < 1 )
      {
      # get the entity body as an array of lines so we can examine it
      my $body = $entity->bodyhandle;
      my @bodylines = $body->as_lines;

      # allow for null parts which would otherwise be detected as unencrypted
      my $bodylinecount= scalar @bodylines;
      md_syslog('debug',"Body check - $bodylinecount lines in $fname/$type");
      if ( $bodylinecount < 5 )
  	  {
	  $msgencrypted=1;
	  }
      }	   # end < 1

    # now check that encryption has been used if it is required
    if ( $enccount > 0 )
      {
      if ( $msgencrypted < 1 )
	  {
        md_syslog('warning',"Unencrypted message to/from Mandatory-encrypted domain $rcptdom, sender=$Sender, Recips = @Recipients");
        action_notify_administrator("Unencrypted mail from $Sender to mandatory encrypted domain, Encryption required for $enccount domains, encryption check returns $msgencrypted, recips=@Recipients");
        action_quarantine_entire_message("Sending unencrypted messages between $senddom and $rcptdom not allowed - see http://pgp.ionixpharma.com/policy.htm for details.\nSender:$Sender\nRecipients = @Recipients\n");
        action_bounce("    ***   Sending unencrypted messages between $senddom and $rcptdom not allowed - see http://pgp.ionixpharma.com/policy.htm for details.  Please also ensure that you DO NOT send mail in HTML format.");
	  }
      }
-------------- next part --------------
#############
# PGP Checks
#############
sub pgp_check($$$)
{
my ($entity,$fname,$type) = @_;

use Mail::GPG;
my $pass;

open(PASS,"</home/defang/.gpgpass") || (md_syslog('warning',"Cannot open GNUPG passphrase file") && return -1);

$pass=<PASS>;
my $gpg = Mail::GPG->new(default_key_id=>'E3AA17BD', default_passphrase=>$pass,
debug=>1,
gnupg_hash_init=>{ armor   => 1,
                   batch   => 1,
				      homedir => '/home/defang/.gnupg' });


# try to use GNUPG to work out the keyholders (!)
$encrypted = $gpg->is_encrypted (entity => $entity);
md_syslog('debug',"PGP_CHECK debug - $fname($type) encrypted = $encrypted");

if ( $fname =~ /.pgp$/i )
  {
  $encrypted=1;		# assume any .PGP file is encrypted
  md_syslog('debug',"$MsgID: PGP - return 0 on $fname($type), .PGP attachment assumed to be OK, $Sender to @Recipients, Subject=$Subject\n");
  return 0;
  }

# get the entity body as an array of lines so we can examine it
my $body = $entity->bodyhandle;
my @bodylines = $body->as_lines;
my $bodysize= scalar @bodylines;

# very small parts are unlikely to be a security risk, so ignore them
if ( $bodysize < 3 )
  {
  md_syslog('debug',"PGP_CHECK - minimal body, return -1, bodylines=$bodysize");
  return -1;
  }

my $pgpstart = scalar grep /--BEGIN/, at bodylines;
$pgpstart += scalar grep / PGP /, at bodylines;
$pgpstart -= scalar grep / PGP SIGN/, at bodylines;
my $pgpend = scalar grep /--END/, at bodylines;

if ( $pgpstart > 5 )
  {
  md_syslog('debug',"PGP_CHECK - PGP found, but too far into message - pgpstart=$pgpstart, pgpend=$pgpend, bodylines=$bodysize");
  return -2;
  }

if ( $type eq "text/html" )
  {
  # encrypted if PGP boundaries found
  if ( ($pgpstart + $pgpend > 2)  && ($pgpstart < $pgpend) )
    {
    $encrypted=1;
    md_syslog('debug',"PGP_CHECK - return 0, html part detected to be encrypted with pgpstart=$pgpstart, pgpend=$pgpend, bodylines=$bodysize");
    return 0 ;
    }
  else
    {
    $encrypted=0;
    md_syslog('debug',"PGP_CHECK - html part NOT encrypted, return -3, pgpstart=$pgpstart, pgpend=$pgpend, bodylines=$bodysize");
    return -3 ;
    }
  }

if ( $encrypted)
  {
  md_syslog('info',"$MsgID: PGP_CHECK detected encrypted part from $Sender, $fname,$ext,$type");
  ($decrypted_entity, $result) = $gpg->decrypt (
             entity     => $entity,
             passphrase => $pass 
             );
	     
  md_syslog('info',"$MsgID: PGP_CHECK decryption completed on $fname,$ext,$type");
  $stderr_sref         = $result->get_gpg_stderr;
  $decryption_ok       = $result->get_enc_ok;
  if ( defined ($decrypted_entity))
    {
    md_syslog('debug',"Entity defined - Decryption: $decryption_ok, STDERR: $$stderr_sref");
    }
  else
    {
    md_syslog('debug',"Entity NOT defined - Decryption: $decryption_ok, STDERR: $$stderr_sref");
    }
  
  if ( defined ($decrypted_entity))
    {
    $decryption_ok       = $result->get_enc_ok;
    $encryption_key_id   = $result->get_enc_key_id;
    $encryption_mail     = $result->get_enc_mail;
    $signed              = $result->get_is_signed;
    $signature_ok        = $result->get_sign_ok;
    $signed_key          = $result->get_sign_key_id;
    $signed_mail         = $result->get_sign_mail;
    $signed_mail_aliases = $result->get_sign_mail_aliases;
    $stdout_sref         = $result->get_gpg_stdout;
    $stderr_sref         = $result->get_gpg_stderr;
    $gpg_exit_code       = $result->get_gpg_rc;
    md_syslog('info',"$MsgID: PGP_CHECK decrypted part from $Sender, Subj=$Subject, $fname,$ext,$type, result=$decryption_ok, keyid=$encryption_key_id,keymail=$encryption_mail");
  
    if ($decryption_ok)
      {
      ($key_id, $key_mail) = $gpg->query_keyring ( search => $encryption_key_id );
      $encrypter="$key_mail/$encrytion_mail/$key_id";
      if ($signed)
        {
        $signer="$signed_mail/$signed_key";
        }
      $i=1;
      $keyrecips="";
      foreach $line( split(/\n/,$$stderr_sref) )
        {
        if ( $line =~ /encrypted with/ )
          {
          @words = split / /, $line;
          $key= $words[7];
          $key=~ s/,//g;
	  if ( defined ($key) )
	    {
            $keyrecips=$keyrecips."$i: $key";
	    }
	  else
	    {
	    $keyrecips=$keyrecips."$i: unknown key";
	    }
          ($key_id, $key_mail) = $gpg->query_keyring ( search => $key );
          $keyrecips=$keyrecips."/$key_mail ";
          $i++;
          }
        }
      md_syslog('debug',"$MsgID: PGP - $Sender, at Recipients,$Subject,$encrypter,$signer,$keyrecips");
      if ($keyrecips =~ /Ionix Pharmaceuticals Ltd/ )
	{
        md_syslog('debug',"$MsgID: PGP - return 0 on $fname($type), Ionix key decrypts $Sender to @Recipients, Subject=$Subject\n");
        return 0;
	}
      else
	{
        md_syslog('debug',"$MsgID: PGP - return 2 on $fname($type), No key to decrypt $Sender to @Recipients, Subject=$Subject\n");
        return 2;
	}
      }
    else
      {
      md_syslog('debug',"$MsgID: PGP_CHECK - return 1 on $fname($type), cannot decrypt");
      return 1;	
      }
    }	# if decrypted OK
  else
    {
    md_syslog('debug',"$MsgID: PGP - return 2 on $fname($type), No key to decrypt $Sender to @Recipients, Subject=$Subject\n");
    return 2;
    }
  }
else
  {
  md_syslog('debug',"$MsgID: PGP - return -3 on $fname($type), not encrypted (encrypted=$encrypted) $Sender to @Recipients, Subject=$Subject ");
  return -3;
  }
}


More information about the MIMEDefang mailing list