2 марта 2015 в 14:54

Конструируем локальный криптографический TLS-прокси c HTTP API электронной подписи



Электронные торги, госуслуги, системы корпоративного электронного документооборота, дистанционного банковского обслуживания и другие подобные системы находятся в сфере интересов регуляторов и вынуждены использовать российские криптосредства для обеспечения конфиденциальности и юридической значимости.

Основным средством взаимодействия пользователя и информационной системы медленно, но верно становится браузер.
Если внимательно рассмотреть вопрос интеграции популярных браузеров и российских криптосредств, то вырисовываются следующие проблемы:
  • Браузеры используют совершенно различные криптографические библиотеки (MS Crypto API, NSS, openssl). Универсального криптосредства, которое «добавляет» ГОСТы во все эти библиотеки нет
  • Механизмы встраивания многих криптосредств в операционную систему и в браузер «завязаны» на версию ОС. С выходом обновления ОС работоспособность криптосредства может кончиться
  • Google Chrome отказывается от поддержки плагинов, работающих через NPAPI. А многие российские вендоры криптосредств разработали плагины, используя именно данный механизм
  • На мобильных платформах браузеры не поддерживают плагины


В данной ситуации наиболее универсальным решением представляется вынести реализацию TLS-ГОСТ и функций ЭЦП в отдельное сетевое приложение, которое принимает запросы от браузера на localhost, позволяет «туннелировать» соединения между браузером и удаленными web-серверами (real proxy), а также предоставляет HTTP API для функционала ЭЦП, работы с сертификатами, токенами и т.п.
Не скажу, что идея является новой, но давайте попробуем сделать некоторый конструктор для ее реализации.

Деталями конструктора будут:


Любители модульных архитектурных экпериментальных решений приглашаются под кат.



Главной идеей решения является то, что часть HTTP-запросов к информационной web-системе обрабатывается удаленным web-сервером, а часть локальным.
Для того, чтобы это стало возможным, на localhost следует запустить web-сервер, который сконфигурирован таким образом, что запросы, содержащие в URL «local», он отсылает на локальное web-приложение, а запросы, содержащие в URL «remote», реверсирует в соответствии со своим конфигурационным файлом. Локальное web-приложение «слушает» по адресам «local/login» и «local/api».

Запросы на адрес «local/api» — это набор специфицированных POST-запросов, которые предоставляют HTTP API для:
  • работы с подключенными Рутокен ЭЦП
  • работы с цифровыми сертификатами X.509 открытого ключа ГОСТ Р 34.10-2001, хранящимися на Рутокен ЭЦП
  • работы с ключевыми парами ГОСТ Р 34.10-2001, хранящимися на Рутокен ЭЦП
  • формирования подписанных и зашифрованных сообщений CMS
  • формирования «сырой» подписи по ГОСТ Р 34.10-2001
  • формирования запросов на сертификат в формате PKCS#10


Запрос на адрес «local/login» обрабатывается локальным web-приложением, которое предоставляет web-интерфейс для авторизации на Рутокен ЭЦП Flash, так как для установки TLS-соединения с удаленным сервером требуется участие токена. После авторизации на устройстве локальное web-приложение «поднимает» TLS-туннель до удаленного сервера. С удаленного сервера браузер получает web-страницы, которые могут использовать описанный криптографический HTTP API, отсылая специальные запросы на адрес «local/api».

Принципиальная схема решения приведена на картинке:


Рассмотрим подробнее компоненты решения.

Web-сервер NGINX

Этот web-сервер вполне успешно запускается на localhost без установки, с FLASH-памяти Рутокен ЭЦП Flash.

Конфигурационный файл выглядит так:
worker_processes  1;

events 
{
worker_connections  1024;
}

http 
{
include mime.types;
default_type application/octet-stream;
sendfile on;
client_max_body_size 0;
access_log nul;
error_log logs/error.log warn;
log_not_found off;
keepalive_timeout 5;
affect the upstream waiting time.
gzip on;
include headers.conf;
include firewall.conf;
include upstream.conf;

server 
{
listen 8000;
root www;
index index.php;

location / 
{
expires -1;
try_files $uri $uri/ /index.php;
}

location /local/login 
{
root www;
try_files $uri /login.php;
}

error_page   500  502  503  504  /50x.html;

location  =  /50x.html 
{
root         html;
}

location ~ \.php$ 
{
root                 www;
try_files           $uri =404;
include            fastcgi_pass.conf;
fastcgi_index   index.php;
include            fastcgi.conf;
#limit_req        zone=req_limit_per_ip  burst=10  nodelay;  #A bug found on Windows 8.
}

location /remote/ 
{
proxy_pass http://localhost:1443;
}

location ~* /rewrite+\. 
{
deny  all;
}
}
include  vhosts/*.conf;
}


Секция
location /remote/ 
{
proxy_pass http://localhost:1443;
} 


Включает режим проброса запросов на localhost:1443.
А на localhost:1443 их в свою очередь принимает локальный TLS-проски, который, используя контроллер Рутокен ЭЦП Flash, устанавливает соединение TLS-ГОСТ в соответствии со своим конфигурационным файлом, аутентифицирует пользователя по цифровому сертификату в рамках протокола TLS и форвардит запросы на удаленный сервер и обратно — через NGINX к браузеру пользователя.

sTunnel


При заходе на URL local/login в случае успешной авторизации пользователя на Рутокен ЭЦП Flash локальное web-приложение, используя php-cgi, запускает процесс sTunnel, передавая ему PIN-код токена в качестве одного из параметров командной строки.
Кроме того, локальное WEB-приложение может сконфигурировать sTunnel на использование определенного сертификата для проведения клиентской аутентификации в рамках TLS.

STunnel, слушая на localhost, ожидает реверcированного NGINX-ом запроса от браузера. При получении первого запроса устанавливает TLS-соединение с удаленным сервером в соответствии со своим конфигурационным файлом и затем передает по установленному соединению запросы и ответы.



Пример конфигурационного файла:

; проверка сертификата сервера обязательна
verify=1

; режим работы: клиент TLS-прокси
client=yes

; корневой сертификат
CAFile=ca2001_A-root.crt

; версия протокола TLS
sslVersion=TLSv1

; не создаем иконку в трее
taskbar=no

; уровень ведения логирования
DEBUG=7
output = log

; использовать engine PKCS11_GOST для работы с Рутокен ЭЦП
engine=pkcs11_gost

; использовать библиотеку PKCS#11 из директории запуска процесс
engineCtrl=MODULE_PATH:rtpkcs11ecp.dll

;за ГОСТ-ы отвечает engine
engineDefault=ALL

  
[remote system]

; используем для загрузки ключа engine PKCS11_GOST
engineNum = 1

; загружаем клиентский сертификат из файла
cert=client.crt

; ID ключевой пары на Рутокен ЭЦП                                                                  
key = a0:59:d9:62:0d:09:69:2f:b6:ba:2d:9b:da:5b:2b:4d:fe:75:05:19

; прокси принимает подключения на данном порту
accept = localhost:1443

; удаленный сервер
connect = x.x.x.x:443
sni = localhost


; используем российский шифрсьют для TLS
ciphers = GOST2001-GOST89-GOST89

; for IE
TIMEOUTclose = 0


Подобное конфигурирование дает вот что:
  • Соединение устанавливается с использованием шифрсьюта GOST2001-GOST89-GOST89
  • Выработка ключа согласования по схеме VKO GOST 34.10-2001 происходит на контроллере Рутокен ЭЦП с применением неизвлекамого ключа аутентификации


Модуль HTTP API


Модуль HTTP API реализуется локальным web-приложением, написанным на PHP с применением PHP-CGI. Следует отметить, что PHP также успешно запускается на localhost с FLASH-памяти Рутокен ЭЦП Flash.

Кратко о реализации. По адресу /local/api web-приложение слушает входящие POST-запросы в формате json.
Каждый запрос должен иметь поля func и prm. Func – имя требуемого метода, prm – массив с параметрами для метода. Что-то типа { func: ‘ someFunc‘, prm: [‘param1’, ‘param2’, …, ‘paramN’] }

В ответ web-приложение отдает стандартный объект, содержащий поля кода возврата/ошибки и, возможно, результата — { res: resultObject, retcode: int}

Для реализации методов нам нужно получить из PHP доступ к функциям Рутокен ЭЦП. Для этой цели будем использовать библиотеку C++, которая содержит в себе как функционал работы с токенами, так и поддержку цифровых сертификатов, запросов на сертификаты и сообщений CMS.
С помощью проекта swig из этой библиотеки удалось сделать модуль PHP («PHP-PKI-Core»), функции которого вызываются посредством механизма PHP-CGI.

Ниже приведен скрипт, который реализует криптографической HTTP API:

<?php
session_start();


if (isset($_SERVER['HTTP_ORIGIN'])) {
    header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
}
header('Access-Control-Allow-Credentials: true');
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization");

if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
    header("HTTP/1.1 200 OK");
    exit();
}


header('Content-type: application/json; charset=UTF-8');
$result = new stdClass();

$valid_chars = array('_');

// получаем запрос json
$request = json_decode(file_get_contents('php://input'), true);


if (isset($request["func"]) && ctype_alnum(str_replace($valid_chars, '', $request["func"]))) {

    // массив параметров
    $result->prm = $request["prm"];

    // название функции
    $result->func = $request["func"];

    // враппер для функций криптобиблиотеки, automatically generated by SWIG (http://www.swig.org)
    include_once("pkiWrapper.php");
    $pkicore = new _CryptoCore();

    try {
        // всегда делаем енумерэйт, ибо хэндлер не сохраняется между запросами
        $pkicore->enumerateDevices();

        // если это логин - сохраняем PIN-код и  ID токена в сессии
        if ($result->func == 'login') {
            call_user_func_array(array($pkicore, $result->func), $result->prm);
            $_SESSION['pin'] = $result->prm[1];
            $_SESSION['tokenid'] = $result->prm[0];
            $result->retcode = 1;
        } else if (isset($_SESSION["pin"]) && isset($_SESSION['tokenid'])) {

            // логин на токен
            $pkicore->login($_SESSION['tokenid'], $_SESSION['pin']);

            // вызов функции с параметрами
            $result->res = call_user_func_array(array($pkicore, $result->func), $result->prm);
            $result->retcode = 1;
        } else {
            // не залогинены на токен, выдаем
            $result->retcode = 1000;
            $result->error = "Токен не залогинен";
        }

    } catch (Exception $ex) {
        $result->retcode = 500;
        $result->error = $ex->getMessage();
        unset($_SESSION['pin']);
        unset($_SESSION['tokenid']);
    }


} else {
    $result->retcode = 500;
    $result->error = 'No function / bad name';
    $result->text = $request["func"];

}

// отдаем результат. 
echo json_encode($result);

?>

Основным нюансом здесь является то, что при обработке каждого HTTP-запроса требуется заново производить логин на токен. Что, в свою очередь, требует хранения ID токена и его PIN-кода в сессии. С точки зрения требований безопасности сессию нужно хранить в оперативной памяти, а не во временном файле.

Вот так примерно выглядит запрос к функции sign HTTP API:
POST /local/api HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 884
Accept: application/json, text/plain, */*
Origin: http://localhost:1443
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36
Content-Type: application/json;charset=UTF-8
Referer: http://localhost:1443/
Accept-Encoding: gzip, deflate
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: PHPSESSID=uq7jdu714nb9f4jec3plc6lpp2; .ASPXAUTH=F33760B6909CE6628C96863CF87B3B1477B161EAB8F603594B4FCB22F182AE083869BEFDD62EF5DB54FA9E3005D3BB2FB7F1400D8BE40C74B522ACD64C427A7D6976AA65E98EB413F2F706F619186D2CF8D6D976961EF09D5742DC6812F15AA5F5ABB68516B0214DED7774C1664FAB7C

{"func":"sign","prm":{"0":0,"1":"dc:48:b8:f2:cf:a8:36:0d:bd:37:33:61:f7:32:ff:1b:a7:65:db:ce","2":"<!PINPADFILE UTF8><N>Платежное поручение<V>2245<N>Сумма<V>71269<N>Дата<V>11.12.2014<N>Получатель<V>ФГУП СервисКонтакт<N>Инн<V>7707083893<N>КПП<V> 775003035<N>Назначение платежа<V>За телематические услуги<N>Банк получателя<V>>ЮгБанк<N>БИК<V>044525225<N>Номер счета получателя<V>30101810400000000225<N>Плательщик<V>ЗАО \"Актив-софт\"<N>Банк плательщика<V>Банк ВТБ (открытое акционерное общество)<N>БИК<V>044525187<N>Номер счета плательщика<V>30101810700000000187","3":false,"4":{"detached":true,"addUserCertificate":true,"useHardwareHash":true}}}


Ответ:
HTTP/1.1 200 OK
Server: nginx/1.4.4
Date: Thu, 26 Feb 2015 10:19:53 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Access-Control-Allow-Origin: http://localhost:1443
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization

{"prm":[0,"dc:48:b8:f2:cf:a8:36:0d:bd:37:33:61:f7:32:ff:1b:a7:65:db:ce","<!PINPADFILE UTF8><N>\u041f\u043b\u0430\u0442\u0435\u0436\u043d\u043e\u0435 \u043f\u043e\u0440\u0443\u0447\u0435\u043d\u0438\u0435<V>2245<N>\u0421\u0443\u043c\u043c\u0430<V>71269<N>\u0414\u0430\u0442\u0430<V>11.12.2014<N>\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u044c<V>\u0424\u0413\u0423\u041f \u0421\u0435\u0440\u0432\u0438\u0441\u041a\u043e\u043d\u0442\u0430\u043a\u0442<N>\u0418\u043d\u043d<V>7707083893<N>\u041a\u041f\u041f<V> 775003035<N>\u041d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043b\u0430\u0442\u0435\u0436\u0430<V>\u0417\u0430 \u0442\u0435\u043b\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0443\u0441\u043b\u0443\u0433\u0438<N>\u0411\u0430\u043d\u043a \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u044f<V>\u0421\u0431\u0435\u0440\u0431\u0430\u043d\u043a<N>\u0411\u0418\u041a<V>044525225<N>\u041d\u043e\u043c\u0435\u0440 \u0441\u0447\u0435\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u044f<V>30101810400000000225<N>\u041f\u043b\u0430\u0442\u0435\u043b\u044c\u0449\u0438\u043a<V>\u0417\u0410\u041e \"\u0410\u043a\u0442\u0438\u0432-\u0441\u043e\u0444\u0442\"<N>\u0411\u0430\u043d\u043a \u043f\u043b\u0430\u0442\u0435\u043b\u044c\u0449\u0438\u043a\u0430<V>\u0411\u0430\u043d\u043a \u0412\u0422\u0411 (\u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0435 \u0430\u043a\u0446\u0438\u043e\u043d\u0435\u0440\u043d\u043e\u0435 \u043e\u0431\u0449\u0435\u0441\u0442\u0432\u043e)<N>\u0411\u0418\u041a<V>044525187<N>\u041d\u043e\u043c\u0435\u0440 \u0441\u0447\u0435\u0442\u0430 \u043f\u043b\u0430\u0442\u0435\u043b\u044c\u0449\u0438\u043a\u0430<V>30101810700000000187",false,{"detached":true,"addUserCertificate":true,"useHardwareHash":true}],"func":"sign","res":"MIIDmQYJKoZIhvcNAQcCoIIDijCCA4YCAQExDDAKBgYqhQMCAgkFADALBgkqhkiG 9w0BBwGgggGnMIIBozCCAVCgAwIBAgIBATAKBgYqhQMCAgMFADBUMQswCQYDVQQG EwJSVTEPMA0GA1UEBxMGTW9zY293MSIwIAYDVQQKFBlPT08gIkdhcmFudC1QYXJr LVRlbGVjb20iMRAwDgYDVQQDEwdUZXN0IENBMB4XDTE1MDIyNjEzMzU1MloXDTE2 MDIyNjEzMzU1MlowGTEXMBUGA1UEAx4OBDIEQwRGBDIEQwRGBDIwYzAcBgYqhQMC AhMwEgYHKoUDAgIjAQYHKoUDAgIeAQNDAARAwXoeizbUWzwA1mkNfWpSQ8eslAIZ dpvMv7qM0n9KTehYyEj1+vkAmMZ9UOT3cE1C6E4fUjWJgXHJL6Ttu5rw86NEMEIw JQYDVR0lBB4wHAYIKwYBBQUHAwIGCCsGAQUFBwMEBgYpAQEBAQIwCwYDVR0PBAQD AgKkMAwGA1UdEwEB/wQCMAAwCgYGKoUDAgIDBQADQQBH8ihBKIWIILO3JJu4j4Bk NN5lQ4n5Y0HUozpHwMfCvR98rLHMmjGFwjdQHHXSrW6eCHryVD8oeV47+hO/U5HM MYIBuTCCAbUCAQEwWTBUMQswCQYDVQQGEwJSVTEPMA0GA1UEBxMGTW9zY293MSIw IAYDVQQKFBlPT08gIkdhcmFudC1QYXJrLVRlbGVjb20iMRAwDgYDVQQDEwdUZXN0 IENBAgEBMAoGBiqFAwICCQUAoIH6MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw HAYJKoZIhvcNAQkFMQ8XDTE1MDIyNjEzMzYxNVowLwYJKoZIhvcNAQkEMSIEIIF3 veehPcgZGmj4rvJQtNwSYf7VzDZNraAiDR89eoysMIGOBgkqhkiG9w0BCQ8xgYAw fjALBglghkgBZQMEASowCAYGKoUDAgIJMAgGBiqFAwICFTALBglghkgBZQMEARYw CwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG 9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIBKDAKBgYqhQMCAhMFAARAZGY6 ZjA6MzU6ZDY6N2M6MjI6ZjA6MDc6ZDc6OGU6NDY6Nzk6YzU6YzU6NmU6MmQ6YzA6 ODg6ZWU6MDU6NjU6NA==y}


В поле res находится подпись в формате CMS detached от переданных данных.

Хочется отметить, что используя модуль PHP-PKI-Core, разработчик может сделать криптографический HTTP API так, как ему удобно.

Заключение


Конструкция, при всей ее кажущейся громоздкости, представляет собой 4 запущенных приложения — nginx, sTunnel, php, php-cgi.
Эти приложения, как я уже отмечал, не требуют установки, хранятся на FLASH-памяти и с нее же запускаются.
Не составляет большого труда написать управляющее приложение, которое аккуратно их стартует и «гасит» процессы при окончании работы или в случае отсоединения токена.
Автор: @cryptoman
«Актив»
рейтинг 88,16

Комментарии (4)

  • +2
    Воу, call_user_func_array с пользовательским вводом? Судя по коду для получения RCE нужно быть авторизованным и отправить

    {"func":"exec","prm":{"0":"id"}}
    

    Проверил локально (отключив pkicore, так как нет него), получил

    {"prm":["id"],"func":"exec","res":"uid=33(www-data) gid=33(www-data) groups=33(www-data)","retcode":1}
    • +1
      А зачем отключать pkicore? Вы же поменяли исходники и сами себе багу создали.
      Без отключения будет
      warning call_user_func_array() expects parameter 1 to be a valid callback, class '_CryptoCore' does not have a method 'exec' in 
      

      здесь скорее надо проверить метод на method_exists
      • +1
        Верно, поспешил с комментом.
        В документации описали подобную ситуацию (когда первый параметр массив) только в примерах — php.net/manual/en/function.call-user-func-array.php, тонко намекая, что так можно.
  • 0
    Вопрос немного не по теме — есть локальное приложение (клиент) способное установить только HTTP соединение с удалённым web сервисом и далее обмен SOAP. А нужно, чтобы оно поддерживало по HTTPS. Что использовать в качестве TLS proxy в качесте клиента, т.е. отправляющего внутренние запросы наружу?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка