Безопасная печать с PGP
Graham Jenkins
Перевод: (C) Александр
Куприн
Далее статья демонстрировала, что функциональность сервера печати Brother можно реализовать в несложной программе на Perl, которая периодически опрашивает POP3-сервер в поисках заданий печати с полностью прибывшими составными частями. При обнаружении такого задания его составные части загружаются одна за другой и декодируются для печати.
В следующей статье "Linux-клиент для Интернет-протокола печати Brother" описана простейшая программа-клиент, которая может быть использована на рабочих станциях Linux для отправки заданий на сервер печати Brother. Программа была реализована в виде shell-скрипта, который разбивал входящий поток на части и размещал их во временной директории для последующего кодирования и отправки.
С тех пор я разработал программу-клиент на Perl, которая обрабатывает входящий поток на лету и не нуждается во временных файлах. Это, несомненно, более правильный подход к решению проблемы. Оборотной стороной оказалась невозможность установить общее число частей, на которое разбивается задание, до тех пор, пока не обработан последний фрагмент. Для того, чтобы программа-сервер смогла работать с пустыми полями "total-parts" в заголовках всех писем, кроме последнего, ее потребовалось слегка изменить.
Можно спросить, зачем мы создаём себе проблемы, разделяя большое задание на части, вместо того, чтобы просто его сжать. И действительно, размер очень и очень многих заданий можно значительно уменьшить при помощи компрессии.
Далее, было немало пользователей Windows (и не только), полагавших, что для переносимости всё следует написать на Perl. Были и догматики от стандартов (На самом деле автор использовал другое, более крепкое, выражение -- нацисты от стандартов /the Standards Nazis/. Прим.перев.), которые считали что части заданий следует отправлять, как элементы [entities] 'message/partial', в соответствии с RFC 2046.
Сообщение может быть подписано в 'прозрачной' ('clear') форме: само сообщение посылается, как есть, а цифровая подпись добавляется в его конец. Если вы не выберете режим использования 'прозрачной' подписи, то сообщение (если это принято по умолчанию) будет сжато, а подпись заключена в нём. Это почти то, что нам нужно!
Существует набор модулей Perl (Crypt::OpenPGP), которые могут выполнять необходимые процедуры по установке и проверке цифровых подписей, так что, в принципе, мы можем написать клиентскую и серверную часть с возможностью их последующего портирования на другие платформы. У меня были небольшие трудности с инсталляцией этого набора, т.к. после установки потребовалось установить ещё несколько модулей, а они в свою очередь потребовали установить математический пакет 'PARI-GP'. Тогда я остановил свой выбор на pgp-2.6.3ia; GnuPG-v1.0.6 тоже будет работать с программами из этой статьи.
Есть пара модулей Perl (Crypt::PGPSimple and PGP::Sign), которые могут быть использованы для вызова pgp-2.6.3ia и эквивалентных ему исполняемых модулей, но каждый из них создаёт временные файлы, а это то, чего я пытаюсь, по возможности, избежать.
Это лишь малая часть того, что нужно сделать. Но, используя модуль Perl MIME::Lite, сделать всё это сравнительно легко, что продемонстрировано ниже в программе 'SEPclientPGP.pl'.
Ну, и как мы можем отправить длинное сообщение, которое, для его прохождения через промежуточный почтовый сервер, нужно разбить на части? RFC 3156 говорит нам, что вместо этого мы должны использовать 'message/partial' механизм MIME (RFC 2046). Думаю, что они имели ввиду "также" ("as well"). Программа 'SEPclientPGP.pl' направляет данные через конвейер в программу 'SplitSend.pl', которая (как будет объяснено ниже) извлекает из сообщения строки "To:" и "Subject:" и повторяет их последовательно в каждом сгенерированном компоненте 'message/partial'.
Далее создается описанное выше сообщение multipart MIME, вторая часть которого берётся из конвейера, "запитываемого" модулем PGP. Если исполняемый модуль не находит подходящей кодовой фразы (passphrase) в переменной среды $PGPPASS, то он затребует её в окне терминала.
(Обратите внимание на то, что в примерах путь к Perl -- /usr/local/bin/perl. Прим. перев.)
#!/usr/local/bin/perl -w # @(#) SEPclientPGP.pl Программа-клиент печати с повышенной безопасностью. (См: RFC 3156) # Обрабатывает данные со стандартного входа и генерирует # сообщение с PGP-подписью, которое далее передаётся # через конвейер (pipe) программе, разбивающей его на части # и отправляющей на сервер по электронной почте. # Требуется 'pgp'-программа. # Graham Jenkins, IBM GSA, Дек. 2001. Пересмотрено: 30 декабря. 2001. 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=""} # Размещаем кодовую фразу в переменной среды my $msg = MIME::Lite->new( # и создаём подписанное сообщение. 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); # Через конвейер передаем подписанное сообщение __END__ # в программу, разбивающую его на части # и передающую на сервер
Функция отправки сообщений do_output() должна проставлять в сообщении следующие поля: "To:", "Subject:", "Content-type.id" (уникальный идентификатор сообщения), "Content-type.number" (текущий номер части сообщения) и "Сontent-type.total" (общее количество частей). Поле "Сontent-type.total" требуется только в последней части сообщения. Всё выглядит очень мило, но есть исключение -- мы не знаем, является ли отправляемая часть последней или нет. Обойти это препятствие можно, применив двойную буферизацию: первый буфер ($InpBuf) заполняется в основном цикле, затем в функции do_output() его содержимое копируется во второй буфер ($OutBuf), содержимое которого и копируется в тело сообщения. При этом функция do_output() организована так, что отправляет сообщение содержащее данные от предыдущего цикла (получается, что при первом вызове функции do_output() ничего не отправляется).
Использование в этой программе модуля MIME::Simple будет выглядеть, как стрельба из пушек по воробьям: что нам действительно нужно, так это найти почтовую программу, которая работала бы на любой платформе.
#!/usr/local/bin/perl -w # @(#) SplitSend.pl Разделение на части и отправка электронного сообщения # (См: RFC 1521, 2046). # Graham Jenkins, IBM GSA, Декабрь 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 { # функция отправки сообщения die basename($0)." .. destination undefined!\n" if ! defined($Dest); $Subj = "" if ! defined($Subj); if ($OutBuf ne "") { # Если выходной буфер содержит данные, $Number++; # то увеличиваем значение Number, и проверяем $Total=$Number if $InpBuf eq ""; # не последняя ли это часть в цепочке. print STDERR "Sending part: ", $Number,"/",$Total,"\n"; $part = MIME::Lite->new( To => $Dest, # Конструируем сообщение. Subject => $Subj, Type => 'message/partial', Encoding=> '7bit', # (Изменить на 8bit! Прим.перев.) 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; # Отправить сообщение. } $OutBuf = $InpBuf # Скопировать содержимое входного буфера $InpBuf = "" # в выходной буфер и выйти из процедуры. } while (<STDIN>) { # Основной цикл. 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} # Сброс обоих буферов и выход из программы. __END__
Полный текст программы 'SEPserverPGP.pl' находится здесь. Я не собираюсь надоедать вам, приводя его, т.к. большая его часть уже фигурировала в статье "Печать через Интернет - альтернатива".
В общих чертах всё это выглядит следующим образом: программа запускается из '/etc/inittab' и работает в цикле каждые полминуты (Точнее интервал задержки между циклами 30 секунд, в то время, как длительность рабочего цикла неопределённа. Прим. перев.). В каждом из циклов она опрашивает на POP3-сервере почтовые ящики одного или нескольких принтеров. Если обнаруживаются части заданий с истекшим сроком давности (Согласно примеру -- 3 дня. Прим. перев.), то они удаляются до того, как информация о них (уникальный идентификатор сообщения и его порядковый номер) будет занесена в таблицу. Если обнаруживается полный набор сообщений для задания на печать, то все части по порядку принимаются с POP3-сервера и отправляются в конвейер. Приведенный ниже фрагмент программы показывает, что происходит потом.
Считается, что в сообщении актуальное содержание начинается со строки "-----BEGIN.." в первой части. В последующих частях оно начинается после пустой строки, следующей за строкой, содержащей "id=..".
Скомпонованное сообщение через конвейер передаётся исполняемому модулю PGP для проверки и раскодирования, а далее на печать соответствующему принтеру. Результаты проверки на корректность направляются во временный файл и затем восстанавливаются оттуда для записи в логах. В том случае, если при проверке выявлены ошибки, вывод на принтер не производится.
for($k=1;$k<=$tp{$part[0]};$k++){ # Проверяем все ли части сообщения собраны. 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++){ # Компонуем части сообщения и передаём в конвейер.$message=$pop->get($slot{$part[0]."=".$k}); $l=0; $buffer=""; $print="N"; while ( defined(@$message[$l]) ) { chomp @$message[$l]; # Часть 1: начинается со строки "-----BEGIN", if( $k == 1 ) { # стоп перед 2-й пустой строкой. 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" } } # Части 2,3,..: пропустить 1 пустую строку else { # после "id=", старт; стоп if( $print eq "Y" ) { # перед следующей пустой строкой. 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:}
Дополнительно вы можете воспрепятствовать кому-либо просматривать поступающие на ваш принтер данные в процессе их передачи через Интернет. Надо изменить параметры запуска исполняемого модуля PGP в программе-клиенте так, чтобы данные не только подписывались, но и шифровались открытым ключом сервера; кроме этого, в исполняемый модуль PGP необходимо передавать кодовую фразу и на стороне сервера.
Чтобы обойти это или, чтобы не нарушать законы вашей страны, вы можете воспользоваться GnuPG-v1.0.6 (или более поздней его версией). В программе-клиенте вам понадобится изменить параметры вызова модуля, кодирующего/раскодирующего сообщения. Учтите, что вы не сможете присваивать вашу кодовую фразу переменной среды.
Если вам интересно, то я написал программу-клиент для Windows-машин, использующую GPG, работающую на ActiveState Perl или IndigoPerl и не требующую дополнительных модулей.
Исполняемый модуль 'gpg' может столкнуться с проблемами, как в процессе расшифровки, и так и после неё. В этом случае нужно перенаправить выходной канал во временный файл -- а потом, если расшифровка прошла успешно, послать его (временный файл) на принтер. (За что боролись на то и напоролись 8-). А как же не использование временных файлов? Прим.перев.)
Команда переводчиков:
Владимир Меренков, Александр Михайлов, Иван
Песин, Сергей Скороходов, Александр Саввин, Роман Шумихин, Александр
Куприн
Со всеми предложениями, идеями и комментариями обращайтесь к Сергею Скороходову ([email protected])