Конструируем локальный криптографический 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-памяти и с нее же запускаются.
    Не составляет большого труда написать управляющее приложение, которое аккуратно их стартует и «гасит» процессы при окончании работы или в случае отсоединения токена.
    Метки:
    «Актив» 43,46
    Компания
    Поделиться публикацией

    Вакансии компании «Актив»

    Комментарии 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 в качесте клиента, т.е. отправляющего внутренние запросы наружу?

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

        Самое читаемое