Использование плагина eToken-ЕПГУ для формирования ЭЦП

в 17:15, , рубрики: etoken, Веб-разработка, госуслуги, Железо, криптография, российские интернет технологии, электронное правительство, метки: , , , ,

Это продолжение темы, поднятой в предыдущем посте о Едином Портале Госуслуг (ЕПГУ) и аппаратных ключах ЭЦП eToken-ГОСТ.
Сначала традиционное описание граблей.

Квест номер 1

После того, как я получил токен, мне захотелось вытащить из него сертификат ключа, чтобы в случае чего отдавать заинтересованным лицам для проверки моей ЭЦП.
Грабля первая — ни SDK 4.55, ни SDK 5.1 не хотели признавать eToken ГОСТ инициализированным и с наличествующим ключом.
Грабля вторая — КриптоПро 3.6 — тоже. Что странно — в бланке сертификата указывалось, что ключ сгенерирован КриптоПро CSP 3.6.
В поисках истины хоть чего-то, что могло бы помочь доступиться до содержимого токена, я набрел на аладдиновский же плагин JC-Web.
Плагин опознавал токен, выдавал SN, список сертификатов числом 1 с ID=3 и названием «Certificate». Но не более. Попытка скормить PIN, или передать данные на подпись вызывали исключение.
Оставалась последняя надежда — расковырять плагин, используемый ЕПГУ для целей авторизации. По сути тот же JC-Web, только сильно проще.
И я полез на сайт Госуслуг.

Квест номер 2

Квест номер два оказался сильно проще.
Расковыряв главную страницу портала госуслуг, я выяснил, что у плагина есть 2 основополагающих JS-метода: etgSignData, и etgGetCertificate. Есть еще свойства etgErrorCode, valid и version, но их я рассматривать не буду, ввиду тривиальности последних.
Эти методы в реализации ЕПГУ обернуты функциями, упрощающими доступ к плагину в контексте веб-страницы.
Я честно их скопировал, лишь чуть-чуть подправив.
В итоге получилась вот такая простенькая веб-страничка, позволяющая а) выдернуть сертификат держателя токена (выдается в Base64), и подписать данные. Подпись тоже формируется в виде PKCS#7, завернутого в Base64 кодировку.

Код страницы-примера для работы с eToken ГОСТ через плагин ЕПГУ

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- Для ознакомительного и информационного использования -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Пример использования eToken ГОСТ через плагин ЕПГУ</title>
</head>
<body style="font-size: 11px; font-family: Verdana;">
    <!-- объект плагина ЕПГУ -->
    <object id="etoken" type="application/x-csuser" width="0" height="0" style="overflow: hidden; float: left;">
            <!--<param name="onload" value="pluginLoaded" />-->
    </object>    
     <script type="text/javascript" language="javascript">
      /**
        *  Operations with eToken GOST using CSuser plugin
        *  
        **/
        // функция подписывания данных токеном. первый параметр - строка данных, второй - ПИН-код токена
        // в поле 1 возвращается ЭЦП (PKCS#7, закодированное в Base64), в поле 5 - код ошибки
        window.SignDataByEToken = function(mess, pin) {
            var plugin = eTokenPlugin();
            if (plugin.valid) {
                //возвращаемые данные
                var return_array = new Object();
                return_array[1] = "";   //cms(base64)
                return_array[5] = "";   //errorCode

                if (mess == "") {
                    alert("Нет данных для подписывания");
                    return return_array;
                }

                try {
                    // Вывод CMS
                    return_array[1] = eTokenPlugin().etgSignData(1, 1, pin, mess, 0);
                    if (eTokenPlugin().etgErrorCode == 28) 
                        return_array[1] = eTokenPlugin().etgSignData(1, 99, pin, mess, 0);
                } catch (e) {
                    alert("Невозможно подписать данныеrn" + e.description);
                    return return_array;
                }

                try {
                    // Проверка ошибки подписи
                    return_array[5] = eTokenPlugin().etgErrorCode;
                } catch (e) {
                    alert(e.description);
                    return return_array;
                }
                return return_array;
            }
        }
        // функция доступа к сертификату владельца. первый параметр - ПИН-код токена. Возвращает сертификат в Base64
        window.GetCertificateByEToken = function(pin) {
            var cert = null;
            try {
                if (eTokenPlugin().valid) {
                    cert = eTokenPlugin().etgGetCertificate(1, 1, pin);
                    if (eTokenPlugin().etgErrorCode == 28) {
                        cert = eTokenPlugin().etgGetCertificate(1, 99, pin);
                    }
                }
            } catch (e) {
                alert(e.description);
            }
            return cert;
        }

       // глобальный аксессор к плагину
        window.eTokenPlugin = function() {
            return document.getElementById("etoken");
        };

       // валидация версии плагина. в данном примере не используется
        window.checkPluginVersion = function(version) {
            if (!(eTokenPlugin() && eTokenPlugin().valid)) return false;
            var plugin_version = eTokenPlugin().version.split('.');
            var portal_version = version.split('.');
            if (isNaN(parseInt(plugin_version[0]))) return false;
            if (isNaN(parseInt(plugin_version[1]))) return false;
            if (isNaN(parseInt(plugin_version[2]))) return false;
            if (isNaN(parseInt(portal_version[0]))) return false;
            if (isNaN(parseInt(portal_version[1]))) return false;
            if (isNaN(parseInt(portal_version[2]))) return false;
            if (parseInt(plugin_version[0]) > parseInt(portal_version[0])) return true;
            if (parseInt(plugin_version[0]) < parseInt(portal_version[0])) return false;
            if (parseInt(plugin_version[1]) > parseInt(portal_version[1])) return true;
            if (parseInt(plugin_version[1]) < parseInt(portal_version[1])) return false;
            if (parseInt(plugin_version[2]) == 11 && parseInt(portal_version[2]) == 9) return false;  //9>11 O_o
            if (parseInt(plugin_version[2]) > parseInt(portal_version[2])) return true;
            if (parseInt(plugin_version[2]) < parseInt(portal_version[2])) return false;
            return true;
        }

        // собственно, начинка этого документа
        function doLogin() {
            var PIN = document.getElementById("pin").value;
            var rd = document.getElementById("cleartext").value;            
            var cert = GetCertificateByEToken(PIN);
            var ds = SignDataByEToken(rd, PIN);
            var dstext = "";
            document.getElementById("cert").value = cert;

            for (name in ds) {
                dstext = dstext + name + " : " + ds[name] + "rn";
            }
            document.getElementById("dsig").value = dstext;
        }  
    </script>
    <!-- UI -->
    <div>
        <a id="btnLogin" onclick="doLogin();" style="border : solid 1px black; width : 140px; height : 40 px;" href="#">Выполнить</a><br/><br/>
        <b>PIN-код</b><br/>
        <input type="password" id="pin" style="width : 250px; border : solid 1px black;"/><br/><br/>
        <b>Данные</b><br/>
        <input type="text" id="cleartext" style="width : 250px; border : solid 1px black;"/><br/><br/>        
        <b>Сертификат</b><br/>
        <input type="text" id="cert" style="width : 250px; border : solid 1px black;"/><br/><br/>
        <b>ЭЦП</b><br/>
        <textarea id="dsig" style="width : 600px; height : 300px; border : solid 1px black;"></textarea><br/><br/>
    </div>
</body>
</html>

Автор: ne_kotin

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js