Это продолжение темы, поднятой в предыдущем посте о Едином Портале Госуслуг (ЕПГУ) и аппаратных ключах ЭЦП 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 кодировку.
<!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