
Электронные торги, госуслуги, системы корпоративного электронного документооборота, дистанционного банковского обслуживания и другие подобные системы находятся в сфере интересов регуляторов и вынуждены использовать российские криптосредства для обеспечения конфиденциальности и юридической значимости.
Основным средством взаимодействия пользователя и информационной системы медленно, но верно становится браузер.
Если внимательно рассмотреть вопрос интеграции популярных браузеров и российских криптосредств, то вырисовываются следующие проблемы:
- Браузеры используют совершенно различные криптографические библиотеки (MS Crypto API, NSS, openssl). Универсального криптосредства, которое «добавляет» ГОСТы во все эти библиотеки нет
- Механизмы встраивания многих криптосредств в операционную систему и в браузер «завязаны» на версию ОС. С выходом обновления ОС работоспособность криптосредства может кончиться
- Google Chrome отказывается от поддержки плагинов, работающих через NPAPI. А многие российские вендоры криптосредств разработали плагины, используя именно данный механизм
- На мобильных платформах браузеры не поддерживают плагины
В данной ситуации наиболее универсальным решением представляется вынести реализацию TLS-ГОСТ и функций ЭЦП в отдельное сетевое приложение, которое принимает запросы от браузера на localhost, позволяет «туннелировать» соединения между браузером и удаленными web-серверами (real proxy), а также предоставляет HTTP API для функционала ЭЦП, работы с сертификатами, токенами и т.п.
Не скажу, что идея является новой, но давайте попробуем сделать некоторый конструктор для ее реализации.
Деталями конструктора будут:
- nginx в режиме reverse proxy
- php, php-cgi
- stunnel
- openssl
- Рутокен ЭЦП Flash
- engine pkcs11_gost
- библиотека pkcs#11
- C++ библиотека PKI-Core
Любители модульных архитектурных
Главной идеей решения является то, что часть 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-памяти и с нее же запускаются.
Не составляет большого труда написать управляющее приложение, которое аккуратно их стартует и «гасит» процессы при окончании работы или в случае отсоединения токена.
