LINUX GAZETTE
[ Prev ][ Table of Contents ][ Front Page ][ Talkback ][ FAQ ][ Next ]

"Linux Gazette...making Linux just a little more fun!"


Secure Printing with PGP

By Graham Jenkins


The Brother Internet Print Protocol

A recent article "Internet Printing - Another Way" described a printing protocol which can be used with some Brother printers. It enables users of Windows machines to send a multi-part base-64 encoded print file via email directly to a Brother print server.

The article went on to show how the functionality of the Brother print server can be implemented in simple Perl program which periodically polls a POP3 server to check for jobs whose parts have all arrived. When such a job is detected, its parts are downloaded in sequence and decoded for printing.

A subsequent article "A Linux Client for the Brother Internet Print Protocol" showed a simple client program which can be used on Linux workstations for sending print jobs to a Brother print server. That program was implemented as a shell script which split an incoming stream into parts and placed them in a scratch directory for subsequent encoding and transmission.

I have since developed a Perl client program which processes the incoming stream on-the-fly and requires no temporary storage. This is, of course, a much neater way to do things. The down-side is that there is no way of ascertaining the total part-count until the last part is being processed. A slight modification to the server program was therefore required to accomodate an empty "total-parts" field on all except the final part.

A Hole Big Enough to Drive a Truck Through

The whole arrangement as outlined above has been in use at my place for several months, and has saved us a whole lot of time and trouble. However, as pointed out by one reviewer, what we really have here is a security hole big enough to drive a truck through! Anybody in the whole wide world can send celebrity pictures to your color printer, and there's not a lot you can do about it.

Somebody else asked why we go to the trouble of splitting a large job into parts without first trying to compress it. And indeed there are a great number of jobs whose size can be significantly reduced through compression.

Then there were the Windows (and other) users, who thought that everything should be written in Perl for portability. And the Standards Nazis, who thought that the job parts should be sent as 'message/partial' entities in accordance with RFC 2046.

Who's Printing Pamela Anderson Pictures?

Of all the issues outlined above, the most serious is indubitably that of client authentication. And the solution is blindingly obvious; why not use one of the Public Key Encryption mechanisms now available? What we need here is for the sender to digitally sign the entire message using his private key. Upon receipt at the server, the message can then be authenticated by application of the sender's public key. There's no need for any secret key-entry rites at the server, so the whole server operation can be automated.

A message signed in this fashion can be signed in 'clear' form; the message itself is then sent as is, with a digital signature appended to its end. If you elect not to use 'clear' signing, the message will (if usual defaults are accepted) actually be compressed and the signature will be incorporated therein. This comes pretty close to what we need!

There is a set of Perl modules (Crypt::OpenPGP) which can perform the necessary signature and verification procedures, so we can actually write the entire client and server programs in a portable form. I had some difficulty with installing these, since they require that a number of other modules be installed, and they require the 'PARI-GP' mathematics package. I elected instead to use pgp-2.6.3ia; GnuPG-v1.0.6 will also work with the programs in this article.

There are a couple of Perl modules (Crypt::PGPSimple and PGP::Sign) which can be used to call pgp-2.6.3ia and its equivalent executables, but each of them creates temporary files, and that's something I try to avoid where possible.

Appeasing the Standards Nazis

RFC 3156 ("MIME Security with OpenPGP") describes how the OpenPGP Message Format can be used to provide privacy and authentication using MIME security content types. In particular, it decrees that after signing our message by encrypting it with our private key, we should send it as a 'multipart/encrypted' message. The first part should contain an 'application/pgp-encrypted' message showing a version number in plain-text form; the second part should contain our actual PGP message.

This is a bit over-the-top, but the overhead is small, and the whole deal is easily done using the Perl MIME::Lite module, as shown in the 'SEPclientPGP.pl' program hereunder.

So how do we send a long message which needs to be broken into parts for passage through intermediate mail servers? RFC 3156 tells us we should use the MIME message/partial mechanism (RFC 2046) instead! I think what they actually mean is "as well". So our output from 'SEPclientPGP.pl' is actually fed into the 'SplitSend.pl' program (also hereunder) which extracts the message "To:" and "Subject:" lines and replicates them into each sequentially numbered 'message/partial' component that it generates.

The Client Program

Here's the client program. It's pretty much self-explanatory. A pipe to the 'SplitSend.pl' program is opened for output. If the passphrase is supplied on the command-line (dangerous, but sometimes necessary!), it is planted in an environment variable.

The multipart MIME message as previously described is then constructed, taking its second body part from a pipe fed by the PGP executable. If the executable doesn't find a suitable passphase in the appropriate environment variable, it requests it in a terminal window.

#!/usr/local/bin/perl -w
# @(#) SEPclientPGP.pl	Secure Email Print client program. Ref: RFC 3156.
#			Takes incoming stream and generates PGP-signed message
#			which is piped to split-and-send program for email
#			transmission to server. Requires 'pgp' program.
#			Graham Jenkins, IBM GSA, Dec. 2001. [Rev'd 2001-12-30]

use strict;
use File::Basename;
use MIME::Lite;
use IO::File;
use Env qw(PGPPASS);

die "Usage: ".basename($0)." kb-per-part destination [passphrase]\n".
    " e.g.: ".basename($0)." 16 lp3\@pserv.acme.com \"A secret\" < report.ps\n".
    "       Part-size must be >= 1\n"
  if ( ($#ARGV < 1) or ($#ARGV > 2) or ($ARGV[0] < 1) );

my $fh = new IO::File "| /usr/local/bin/SplitSend.pl $ARGV[0]";
if( defined($ARGV[2]) ) {$PGPPASS=$ARGV[2]}
if( ! defined ($PGPPASS)) {$PGPPASS=""}	# Plant passphrase in environment and
my $msg = MIME::Lite->new(		# create signed message.
                To      => $ARGV[1],
                Subject => 'Secure Email Print Job # '.time,
                Type    => 'multipart/encrypted');
$msg->attr  (   "content-type.protocol" => "pgp-encrypted");
$msg->attach(   Type    => 'application/pgp-encrypted',
                Encoding=> 'binary',
                Data    => "Version: 1\n");
$msg->attach(   Type    => 'application/octet-stream',
                Encoding=> 'binary',
                Path    => "/usr/local/bin/pgp -fas - |");
$msg->print($fh);			# Pipe the signed message into a
__END__					# split-and-send program.

Split-and-Send

Here's the split-and-send program. The main loop at the end works just as described above - extract the destination and subject fields, accumulate lines until we are about to exceed the message-size limit supplied as a parameter, then feed what we have to an output routine.

The output routine needs to re-insert the destination and subject fields, and also insert a message-identifier, part-number and total-part-count. The total-part-count is only required on the final part. All fairly easy - except we don't know whether the current part is the final part until we look for the next part. So we get around this by using a double-buffer arrangement, where we don't actually output a buffer's contents until we have the next buffer.

Using MIME::Simple in this program is really overkill; however, what it does accomplish is that it tries to find an appropriate mailer program on whatever platform it executes.

#!/usr/local/bin/perl -w
# @(#) SplitSend.pl	Splits and sends an email message (Ref: RFC 1521, 2046).
#			Graham Jenkins, IBM GSA, December 2001.

use strict;
use File::Basename;
use MIME::Lite;
use Net::Domain;
my ($Id,$j,$Dest,$Subj,$part,$InpBuf,$OutBuf,$Number,$Total);

die "Usage: ".basename($0)." kb-per-part\n".
    "       Part-size must be >= 1\n" if ( ($#ARGV != 0) or ($ARGV[0] < 1) );

$Id=(getlogin."\@".Net::Domain::hostfqdn().time) or $Id="unknown_user".time;
$Number = 0; $Total = ""; $OutBuf=""; $InpBuf=""; print STDERR "\n";

sub do_output {				# Output subroutine.
  die basename($0)." .. destination undefined!\n" if ! defined($Dest);
  $Subj = ""                                      if ! defined($Subj);
  if ($OutBuf ne "") {			# If output buffer contains data, 
    $Number++;				# increment Number, and check whether
    $Total=$Number if $InpBuf eq "";	# it is the last buffer.
    print STDERR "Sending part: ", $Number,"/",$Total,"\n";
    $part = MIME::Lite->new(
              To      => $Dest,		# Construct a message containing the
              Subject => $Subj,		# output buffer contents.
              Type    => 'message/partial',
              Encoding=> '7bit',
              Data    => $OutBuf);
    $part->attr("content-type.id"     => "$Id");
    $part->attr("content-type.number" => "$Number");
    $part->attr("content-type.total"  => "$Total") if ($Number eq $Total);
    $part->send;			# Send the message.
  }
  $OutBuf = $InpBuf;			# Move input buffer contents to
  $InpBuf = ""				# output buffer and exit.
}

while (<STDIN>) {			# Main loop.
  if ( (substr($_, 0, 3) eq "To:")      && (! defined($Dest)) ) {
    $Dest = substr($_, 4, length($_) - 4); chomp $Dest; next }
  if ( (substr($_, 0, 8) eq "Subject:") && (! defined($Subj)) ) {
    $Subj = substr($_, 9, length($_) - 9); chomp $Subj; next }
  if ( (length($InpBuf . $_)) > ($ARGV[0] * 1024) ) {do_output}
  $InpBuf = $InpBuf . $_
}
foreach $j (1,2) {do_output}		# Flush both buffers and exit.
__END__

The Art of Jigsaw Assembly

There is no guarantee that the segments of our print-job will arrive at the server in the same order as they left the client. We cannot be sure that there will even be the same number of segments, since message-transfer agents along the way are allowed to re-assemble message/partial entities as they see fit. So what we have at the server end is a set of jigsaw puzzles, with the pieces of each puzzle being related by a common message-identifier, and their placement within that puzzle being determined by their part-numbers.

For a full listing of the 'SEPserverPGP.pl', see the attached text version. I haven't bothered to replicate all of it hereunder, since much of it is the same as the program shown in "Internet Printing - Another Way".

Basically, the program is intended for invocation via an entry in '/etc/inittab', and loops continually thereafter, with half-minute pauses between each loop. During each loop, it visits the mailboxes of one or more printer-entities on a POP3 server, and deletes any stale articles therein before tabulating the message-id's and part-numbers of the remaining articles. When it finds a full set of message/partial entities, it sucks each of them in part-number sequence from the server, and throws their contents into a pipe. The program-extract hereunder shows what happens then.

The relevant message content is deemed to begin at the "-----BEGIN.." line in the first part. For subsequent parts, it begins after the first blank line once an "id=.." line has been seen.

Once in the pipe, the composite message content passes to the PGP executable for validation/decryption, and thence to an appropriate printer. Validation output is passed to a scratch file, and then recovered from there for logging. A validation failure results in no output to the printer.

          for ($k=1;$k<=$tp{$part[0]};$k++){	# Check if we have all parts.
            goto I if ! defined($slot{$part[0]."=".$k});
          }					
          $fh=new IO::File
           "| /usr/local/bin/pgp -f 2>$tmp | lpr -P $user >/dev/null" or goto I;
          for ($k=1;$k<=$tp{$part[0]};$k++){	# Assemble parts into pipe. 
            $message=$pop->get($slot{$part[0]."=".$k});
            $l=0; $buffer=""; $print="N";
            while ( defined(@$message[$l]) ) {
              chomp @$message[$l]; 		# Part 1: start at "-----BEGIN",
              if( $k == 1 ) {			# stop before 2nd blank line.
                if( @$message[$l]=~m/^-----BEGIN/ ) { $m=-2;  $print="Y"}
                if( $print eq "Y" ) {
                  if( @$message[$l] eq "" ) { $m++; if( $m >= 0)   {last} } 
                  $buffer=$buffer.@$message[$l]."\n"
                }
              }					# Part 2,3,..: skip 1 blank line
              else {				# after "id=", then start; stop
                if( $print eq "Y" ) {		# before next blank line.
                  if( @$message[$l] eq "" )                        {last} 
                  $buffer=$buffer.@$message[$l]."\n"
                }
                if( @$message[$l]=~m/id=/ )                  {$print="R"}
                if((@$message[$l] eq "") && ($print eq "R")) {$print="Y"}
              }
              $l++;
            }
            print $fh $buffer or goto I;
          }
          $fh->close || goto I;
          open $fh, $tmp;
          while (<$fh>) { chomp; syslog('info', $_) }
          close $fh;
          for ($k=1;$k<=$tp{$part[0]};$k++){
            $pop->delete($slot{$part[0]."=".$k})
          }
          goto I;
        }
J:    }	
    }
I:}

Copycat Crime

In the scheme outlined above, there is nothing to prevent a determined trouble-maker replicating and replaying an entire authenticated message. To cover this possibility, you need to retain each log entry for a week or so, and to reject any incoming message having a corresponding signature and signature-date.

If, in addition, you wish to prevent someone from viewing the actual data travelling to your printer as it traverses the Internet, you need to change the PGP executable parameters at the client end so that the data is encrypted with the server's public key as well as signed; you will also need to feed a passphrase into the PGP executable at the server end.

GNU Privacy Guard

I have a mental image of somebody reading this and saying: "How come he's using pgp-2.6.3ia if he doesn't like un-necessary temporary files?" It's a good question, because pgp-2.6.3ia creates temporary files both during encryption and during decryption.

To get around this, or to comply with whatever laws are applicable in your country, you may wish to use GnuPG-v1.0.6 (or later version of the same) instead. In the client program, you will need to change the parameters with which the executable is called. And you won't be able to plant your passphrase in an environment variable.

I have attached for your interest a 'Lite' GPG client program which will execute on Windows machines with 'out-of-the-box' ActiveState Perl or IndigoPerl, and requires no extra modules.

During decryption to a pipe, the 'gpg' executable actually outputs data to the pipe until (and in some cases, after) it encounters a problem. So you will need to send your output to a scratch file - then send that scratch file to your printer if the decryption process completed satisfactorily.

Graham Jenkins

Graham is a Unix Specialist at IBM Global Services, Australia. He lives in Melbourne and has built and managed many flavors of proprietary and open systems on several hardware platforms.


Copyright © 2002, Graham Jenkins.
Copying license http://www.linuxgazette.net/copying.html
Published in Issue 75 of Linux Gazette, February 2002

[ Prev ][ Table of Contents ][ Front Page ][ Talkback ][ FAQ ][ Next ]