Безопасная печать с PGP
 Graham Jenkins
Перевод: (C) Александр Куприн


Brother - Интернет-протокол печати

В предыдущей статье "Печать через Интернет - альтернатива" описан протокол печати, используемый некоторыми принтерами Brother. Этот протокол позволяет пользователям Windows-машин посылать на сервер печати Brother сообщения электронной почты, содержащие разделённые на части и закодированные в base-64 файлы.

Далее статья демонстрировала, что функциональность сервера печати Brother можно реализовать в несложной программе на Perl, которая периодически опрашивает POP3-сервер в поисках заданий печати с полностью прибывшими составными частями. При обнаружении такого задания его составные части загружаются одна за другой и декодируются для печати.

В следующей статье "Linux-клиент для Интернет-протокола печати Brother" описана простейшая программа-клиент, которая может быть использована на рабочих станциях Linux для отправки заданий на сервер печати Brother. Программа была реализована в виде shell-скрипта, который разбивал входящий поток на части и размещал их во временной директории для последующего кодирования и отправки.

С тех пор я разработал программу-клиент на Perl, которая обрабатывает входящий поток на лету и не нуждается во временных файлах. Это, несомненно, более правильный подход к решению проблемы. Оборотной стороной оказалась невозможность установить общее число частей, на которое разбивается задание, до тех пор, пока не обработан последний фрагмент. Для того, чтобы программа-сервер смогла работать с пустыми полями "total-parts" в заголовках всех писем, кроме последнего, ее потребовалось слегка изменить.

Дыра достаточно большая, чтобы ездить через неё на грузовике

Я использовал решение, описанное в общих чертах выше,  на протяжении нескольких месяцев и оно сэкономило мне уйму времени и избавило от многих хлопот. Тем не менее, как заметил один критик, что мы действительно имеем, так это брешь в безопасности, причем такой величины, что через неё свободно проедет грузовик! Кто угодно может послать на ваш цветной принтер фотографию знаменитости, и вы вряд ли сможете что-нибудь с этим сделать.

Можно спросить, зачем  мы создаём себе проблемы, разделяя большое задание на части, вместо того, чтобы просто его сжать. И действительно, размер очень и очень многих заданий можно значительно уменьшить при помощи компрессии.

Далее, было немало пользователей Windows (и не только), полагавших, что для переносимости всё следует написать на Perl. Были и  догматики от стандартов (На самом деле автор использовал другое, более крепкое, выражение -- нацисты от стандартов /the Standards Nazis/. Прим.перев.), которые считали что части заданий следует отправлять, как элементы [entities] 'message/partial', в соответствии с RFC 2046.

Кто это там печатает фотографии Памелы Андерсон?

Самой серьезной из всех описанных выше проблем , несомненно, является аутентификация клиента. А решение до смешного очевидно: почему бы не воспользоваться одним из доступных сейчас методов шифрования с открытым (public) ключом? Нам нужно, чтобы отправитель, используя секретный (закрытый, private) ключ, поставил цифровую подпись на все сообщение (Подробнее об этом смотрите описание GNU Privacy Guard, глава "Общие понятия",  раздел "Цифровые подписи". Прим. перев.). После получения сообщение может быть аутентифицировано на сервере по открытому ключу отправителя. Поскольку на сервере не требуется совершать никаких "ритуалов" с закрытыми ключами, то процесс может быть полностью автоматизирован.

Сообщение может быть подписано в 'прозрачной' ('clear') форме: само сообщение посылается, как есть, а цифровая подпись добавляется в его конец. Если вы не выберете режим использования 'прозрачной' подписи, то сообщение (если это принято по умолчанию) будет сжато, а подпись заключена в нём. Это почти то, что нам нужно!

Существует набор модулей Perl (Crypt::OpenPGP), которые могут выполнять необходимые процедуры по установке и проверке цифровых подписей, так что, в принципе,  мы можем написать клиентскую и серверную часть с возможностью их последующего портирования на другие платформы. У меня были небольшие трудности с инсталляцией этого набора, т.к. после установки потребовалось установить ещё несколько модулей, а они в свою очередь потребовали установить математический пакет 'PARI-GP'. Тогда я остановил свой выбор на pgp-2.6.3ia; GnuPG-v1.0.6 тоже будет работать с программами из этой статьи.

Есть пара модулей Perl (Crypt::PGPSimple and PGP::Sign), которые могут быть использованы для вызова pgp-2.6.3ia и эквивалентных ему исполняемых модулей, но каждый из них создаёт временные файлы, а это то, чего я пытаюсь, по возможности, избежать.

Умиротворение догматиков стандартизации

RFC 3156 ("MIME Security with OpenPGP") описывает, как можно использовать формат сообщений OpenPGP для обеспечения конфиденциальности и аутентификации при помощи типов безопасного содержимого (security content types) MIME. В частности, декларируется, что после подписания нашего сообщения зашифрованного закрытым ключом, мы должны отправить его, как сообщение 'multipart/encrypted'. Первая часть должна содержать 'application/pgp-encrypted' сообщение и показывать номер версии в форме обычного текста (plain-text); вторая часть должна содержать наше PGP сообщение.

Это лишь малая часть того, что нужно сделать. Но, используя модуль Perl MIME::Lite, сделать всё это сравнительно легко, что продемонстрировано ниже в программе 'SEPclientPGP.pl'.

Ну, и как мы можем отправить длинное сообщение, которое, для его прохождения через промежуточный почтовый сервер, нужно разбить на части? RFC 3156 говорит нам, что вместо этого мы должны использовать 'message/partial' механизм MIME (RFC 2046). Думаю, что они имели ввиду "также" ("as well"). Программа 'SEPclientPGP.pl' направляет данные через конвейер в программу 'SplitSend.pl', которая (как будет объяснено ниже) извлекает из сообщения строки "To:" и "Subject:" и повторяет их последовательно в каждом сгенерированном компоненте 'message/partial'.

Программа-клиент

Это программа-клиент. Она почти не требует объяснений. Для программы 'SplitSend.pl' открывается выходной канал. Если кодовая фраза передаётся через командную строку (опасно, но иногда необходимо!), то она размещается в переменной окружения.

Далее создается описанное выше сообщение 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__                                 #  в программу, разбивающую его на части
                                        #  и передающую на сервер

Разбиение-и-передача

Здесь рассматривается пример программы, разбивающей сообщение на части и отправляющей его. Основной цикл работает так, как было описано выше -- извлекает поля "To:" и "Subject:", накапливает строки до тех пор, пока их количество не превысит указанного в строке параметров и вызывает функцию do_output(), отправляющую накопленный массив по электронной почте.

Функция отправки сообщений 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__

Искусство собирания мозаики

Нет никаких гарантий, что части задания прибудут на сервер в той же последовательности, как их отправит клиент. Нам остаётся только собирать мозаику из кусочков, связанных между собой общим уникальным идентификатором ("Content-type.id")  и разместить их согласно их порядковым номерам ("Content-type.number").

Полный текст программы '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 необходимо передавать кодовую фразу и на стороне сервера.

GNU Privacy Guard

Я представил себе, что кто-то читает это и говорит: "Как же он использует pgp-2.6.3ia, если ему не нравится использовать временные файлы?" Это хороший вопрос, т.к. pgp-2.6.3ia создаёт временные файлы и в процессе кодирования и в процессе раскодирования.

Чтобы обойти это или, чтобы не нарушать законы вашей страны, вы можете воспользоваться GnuPG-v1.0.6 (или более поздней его  версией). В программе-клиенте вам понадобится изменить параметры вызова модуля, кодирующего/раскодирующего сообщения. Учтите, что вы не сможете присваивать вашу кодовую фразу переменной среды.

Если вам интересно, то я написал программу-клиент для Windows-машин, использующую GPG, работающую на ActiveState Perl или IndigoPerl и не требующую дополнительных модулей.

Исполняемый модуль 'gpg' может столкнуться с проблемами, как в процессе расшифровки, и так и после неё. В этом случае нужно перенаправить выходной канал во временный файл -- а потом, если расшифровка прошла успешно, послать его (временный файл) на принтер. (За что боролись на то и напоролись 8-). А как же не использование временных файлов? Прим.перев.)


Graham Jenkins

Graham является специалистом по UNIX в IBM Global Services, Australia. Живет в Мельбурне. За свою жизнь он сконфигурировал и "садминистрировал" широчайший спектр патентованных и открытых систем на различных аппаратных платформах.
Copyright (С) 2002, Graham Jenkins.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 75 of Linux Gazette, February 2002

Команда переводчиков:
Владимир Меренков, Александр Михайлов, Иван Песин, Сергей Скороходов, Александр Саввин, Роман Шумихин, Александр Куприн

Со всеми предложениями, идеями и комментариями обращайтесь к Сергею Скороходову ([email protected])