Это старая редакция страницы Разработки / Движок / Gnu P G за 20/03/2007 20:00.
Общая спецификация интеграции GnuPG [ЧЕРНОВИК]
Задачи
Задачами, решаемыми интеграцией GnuPG, являются:
- Валидация загружаемого пользователем (в свой профиль) открытого ключа на корректность с последующим внесением отпечатка в базу данных.
- Защита приватной связи между пользователями сайта.
- Защита отправляемого пользователю восстановленного пароля.
Возможность аутентификации на сайте с помощью протокола запрос-ответ (подписание пользователем строки запроса, сгенерированной сервером) не определяется в числе задач, поскольку, хотя и скрывает пароль от наблюдателя на канале связи, сама по себе не в силах предотвратить несанкционированный доступ к учетной записи пользователя с помощью перехваченных cookie и иными путями. Решение же данной проблемы — SSL — вообще делает подобную схему избыточной, поскольку защищает как реквизиты авторизованной сессии (cookie/SID) при работе с сайтом, так и реквизиты пользователя (логин/пароль) в момент аутентификации.
Тем не менее, список задач остается открыт для дополнений.
Функции
Для решения означенных задач требуется реализовать следующий набор функций (достаточность и необходимость всех функций из списка подлежит обсуждению).
Загрузка/валидация ключа
Загрузка ключа выполняется на странице персональных настроек уже после регистрации и успешного входа в систему. Загрузка на этапе регистрации не имеет смысла по той причине, что HTTP-POST-запрос как целое не подвергается цифровому подписанию, и потому может быть модифицирован злоумышленником, тогда как использование SSL снимает подобные риски и на этапе регистрации, и последующего логина и изменения настроек.
Все этапы загрузки/валидации ключа выполняются в контексте специальной временной связки. Если импортированный ключ удовлетворяет необходимым условиям (см. ниже), производится его автоматический перенос в основную связку сервера.
UploadPK(keyASCII)
Импортирует загруженный ключ на временную связку. Единственный аргумент, keyASCII, представляет собой загруженный пользователем блок открытого ключа, передаваемый в GnuPG через временный файл с параметром --import. Сразу после исполнения вызывается CheckPK(keyID), которая анализирует ключ с целью определения его пригодности для зашифрования.
RecievePK(keyID[, ksURL])
Импортирует ключ с сервера ключей на временную связку. Передает GnuPG указанный пользователем (в профиле) ID ключа в параметре командной строки --recv-keys. Опциональный аргумент ksURL позволяет пользователю задать URL сервера ключей, вместо дефолтного subkeys.pgp.net. Как и в предыдущем случае сразу после исполнения вызывается CheckPK(keyID).
Вопросы: следует ли перед исполнением команды с параметром --recv-keys исполнить ее с --search-keys и, если сервер вернет более одно ключа, остановить программу и выдать предупрждение (к примеру, чтобы пользователь скорректировал запрос и указал keyID в 16-значном формате)? Альтернативным способом снятия двусмысленности может быть такой: показать пользователю результаты поиска и предложить выбрать принадлежащий ему ключ. Требуется протестировать альфу и изучить статус-коды, возвращаемые в различных описанных режимах.
CheckPK(keyID)
Парсит сертификат ключа (используются параметры --with-colons --list-public-keys <keyID>) и проверяет следующие условия: 1) ключ содержит по крайней мере один шифровальный подключ и 2) базовый ключ и шифровальный подключ в данный момент действительны. Оба условия не имеют критического значения при загрузке ключа (пользователю выводится только предупреждение), но при восстановлении пароля или отправке приватного сообщения операция завершится неудачей.
AcceptPK(keyID)
Вызывается при импорте ключа в случае положительного ответа CheckPK() и переносит импортированный ключ с временной связки на главную.
RejectPK(keyID)
Вызывается при импорте ключа в случае отрицательного ответа CheckPK() и удаляет ключ с временной связки.
DeletePK(keyID)
Удаляет открытый ключ с главной связки.
FlushTemp()
Очищает временную связку ключей.
<...>
Реализация
Ограничивающим фактором в реализации схемы выступает запрет стандартной PHP-функции exec() (что является нормой для любого виртуального хостинга). Таким образом, взаимодействие с GnuPG может осуществляться только через интерфейс CGI. Использовать с этой целью CGI-сборку PHP нерационально, поэтому итоговая конструкция представляет собой:
. openSpace Engine
пользовательский уровень, графический интерфейс
/\
||
\/
openSpace-GPG engine class
функциональная прокладка, реализующая необходимый
набор функций ввода/вывода для взаимодействия с
GnuPG и проверки результатов операций
/\
||
\/
openSpace-GPG CGI wrapper
простейшая Perl-оболочка, "тупо" вызывающая GnuPG
с переданными параметрами командной строки и воз-
вращающая вывод программы верхнему уровню
/\
||
\/
GnuPG executable
исполняемый файл программы, лежащий за пределами
публичной www-директории сервера
Класс openSpace-GPG
<?php
// OPENSPACE-GPG INTEGRATION CLASS version 0.0.2
// (c) 2007 SATtva, http://www.vladmiller.info
// Originally for "openPGP in Russia" http://www.pgpru.com
// Licensed under GNU General Public License (GPL)
// http://opensource.org/licenses/gpl-license.php
// ToDo:
// - a whole pile of!
/*
########################################################
## openSpace-GPG integration class ##
########################################################
NOTE: PHP 5.0.0 or later is required!
*/
class GPG
{
// VARIABLES
var $engine;
var $UID = NULL; // pubkey user ID
var $keyID = NULL; // pubkey ID
var $finger = NULL; // pubkey fingerprint
var $baseurl; // site url with proper protocol
var $homedir; // gpg homedir
var $tempdir; // temporary data dir
var $sessdir; // user session dir
var $cgiwdir = 'cgi-bin/openspace-gpg/wrapper'; // cgi wrapper location
var $stfile; // gpg status file
var $srfile; // gpg stderr file
// CONSTRUCTOR
function __construct(&$engine)
{
// defining object main properties
$this->engine = & $engine;
$this->baseurl = ( $this->engine->config['ssl'] == true ? str_replace('http://', 'https://', $this->engine->config['base_url']) : $this->engine->config['base_url'] );
$this->homedir = rtrim($this->engine->config['gpg_home'], '/');
$this->tempdir = rtrim($this->engine->config['gpg_temp'], '/');
$this->wrapper = trim($this->engine->config['gpg_wrapper'], '/');
$this->sessdir = $this->tempdir.'/'.session_id();
$this->stfile = $this->sessdir.'/status';
$this->srfile = $this->sessdir.'/error';
// creating user session directory
if (false === @mkdir($this->sessdir, 0777, true))
{
die('openSpace-GPG: unable to create user session directory.');
}
}
// call openspace-gpg wrapper
// $request - additional gpg commant line parameters
// (except homedir and status-file)
// $method - passing method: post or get (default)
function Call($request, $method = 'get')
{
if ($method != 'get' && $method != 'post') $method = 'get';
$request = array(
'http' => array(
'method' => $method,
'header' => ( $method == 'post' ? 'Content-type: application/x-www-form-urlencoded' : '' ),
'content' => http_build_query(array(
'hd' => $this->homedir, // homedir
'sf' => $this->stfile, // status-file
'sr' => $this->srfile, // stderr
'cl' => $request) // command line params
) // end of content array
) // end of http array
); // end of request array
$context = stream_context_create($request);
$script = @fopen($this->baseurl.$this->wrapper, 'r', false, $context);
if (!$script)
{
die('openSpace-GPG: unable to open CGI wrapper.');
}
else
{
// reading output till the end
while (false === feof($script))
{
$result .= fgets($script, 1024);
}
fclose($script);
}
// throwing away appended error code value
return substr($result, 0, strrpos($result, "\n"));
}
// get gpg status codes of the last operation
function GetStatus()
{
$statusfile = @fopen($this->stfile, 'r');
// read status file
if (!$statusfile)
{
return false;
}
else
{
while (false === feof($statusfile))
{
$statuscodes .= fread($statusfile, 1024);
}
fclose($statusfile);
}
if ($statuscodes)
{
$rows = explode("\n", str_replace("\r", '', $statuscodes));
foreach ($rows as $row) if (substr($row, 0, 9) == '[GNUPG:] ') $matrix[] = explode(' ', substr($row, 9));
return $matrix;
}
else return false;
}
// get parseable key details listing
function GetList($keyID = '')
{
if (!$keyID) $keyID = $this->finger;
if (!$keyID) $keyID = $this->keyID;
if (!$keyID) return false;
if ($list = $this->Call('--with-colons --fixed-list-mode --with-fingerprint --list-public-keys '.$keyID))
{
$rows = explode("\n", str_replace("\r", '', $list));
foreach ($rows as $row) if (substr($row, 0, 3) != 'tru') $matrix[] = explode(':' $row);
return $matrix;
}
else return false;
}
// define pubkey-related object properties from gpg status
// codes ('status') or key listing ('list', needs $keyID)
function DefineKey($source, $keyID = '')
{
if ($source == 'status')
{
if (false === $status = $this->GetStatus()) return false;
foreach ($status as $code)
{
if ($code[0] == 'IMPORTED')
{
$this->keyID = $code[1];
// recompose primary user ID
$temp = $code;
unset($temp[0], $temp[1]);
$this->UID = implode(' ', $temp);
}
else if ($code[0] == 'IMPORT_OK')
{
$this->keyID = substr($code[2], -16);
$this->finger = $code[2];
}
}
return true;
}
else if ($source == 'list')
{
if (false === $list = $this->GetList($keyID)) return false;
foreach ($list as $row)
{
if ($row[0] == 'pub') $this->keyID = $row[4];
if ($row[0] == 'fpr') $this->finger = $row[9];
if ($row[0] == 'uid' && !$definedUID)
{
$this->UID = $row[9];
$definedUID = true;
}
}
return true;
}
}
// check gpg operation
function SelfCheck()
{
$gpg = $this->Call('--version');
if ($gpg == true)
{
$gpg = substr($gpg, 0, strpos($gpg, "\n"));
if (stristr($gpg, 'gpg (gnupg)') == true) return true;
else return false;
}
else
{
return false;
}
}
// import key onto the temp keyring through webform
function UploadPK($keyblock)
{
// save pubkey into the temp file.
#########################################################
# is there a way to pass it through the wrapper along #
# with other command line parameters in one call? #
#########################################################
$tempfile = @fopen($this->sessdir.'/tempfile', 'w');
if (!$tempfile)
{
die('openSpace-GPG: unable to open temporary file with write access.');
}
else
{
fwrite($tempfile, $keyblock, 262144);
fclose($tempfile);
}
// import key
$this->Call("--no-default-keyring --keyring {$this->tempdir}/tempring --import {$this->sessdir}/tempfile");
}
// makes sure imported key is suitable for encryption
function CheckPK()
{
}
// DESTRUCTOR
function __destruct()
{
// flush session dir
clearstatcache();
if ($dh = opendir($this->sessdir))
{
while (false !== ($filename = readdir($dh)))
{
if (is_dir($file = $this->sessdir.'/'.$filename) !== true) unlink($file);
}
closedir($dh);
rmdir($this->sessdir);
}
}
}
?>
Файл конфигурации gpg.conf
Поскольку GnuPG должен работать в полностью автономном режиме, видится необходимым следующий конфигурационный файл:
CGI-обертка openSpace-GPG
Ничего существенного: