В данном обзоре рассмотрим процесс создания веб-инструмента, который позволит нам подписывать различные файлы электронной подписью.
Веб-инструмент — это сайт. Поэтому нам потребуется сервер, с необходимым ПО и какая то система управления им.
Наш сайт будет написан на языке php.
И так, в качестве серверного ПО будем использовать:
- сервер Linux. С установленным php, apache, nginx. (Необходимые версии и настройки прописаны тут)
- SkeekS CMS
На этом не стоит подробно останавливаться, потому что, бэкенда часть нашего сервиса выполняет минимальные действия (формирование и отдача html страниц).
Поэтому по правилам SkeekS CMS мы сформируем html страницу, на которой будет присутствовать html+css+js код.
Сформированная сайтом страница рендерится в браузере клиента. в момент рендеринга, исполняется js и css код присутствующий на ней.
JavaScript (/ˈdʒɑːvɑːˌskrɪpt/; аббр. JS /ˈdʒeɪ.ɛs./) — прототипно-ориентированный сценарный язык программирования. Является реализацией языка ECMAScript (стандарт ECMA-262[5]).
JavaScript обычно используется как встраиваемый язык для программного доступа к объектам приложений. Наиболее широкое применение находит в браузерах как язык сценариев для приданияинтерактивности веб-страницам.
Собственно наша задача и состоит в написании JavaScript кода, который сможет осуществлять действия с электронной подписью.
Данные действия мы сможем осуществлять с использованием плагина для браузера КриптоПро ЭЦП Browser plug-in
КриптоПро ЭЦП Browser plug-in позволяет реализовать работу с ЭЦП (в том числе и с усовершенствованной ЭЦП) в следующих веб-браузерах:
-
Microsoft Internet Explorer,
-
Mozilla Firefox,
-
Opera,
-
Apple Safari,
-
Google Chrome,
-
другие браузеры, поддерживающие плагины NPAPI.
Поддерживаемые операционные системы:
-
Microsoft Windows XP/2003/Vista/2008/W7/2008R2/W8/2012/8.1/2012R2,
-
Linux,
-
Apple iOS,
-
Apple MacOS.
В КриптоПро ЭЦП Browser plug-in реализован набор объектов, идентичный CADESCOM и набор объектов для работы с запросами на сертификат интерфейс CertEnroll.
CAPICOM (англ. Crypto API COM-object) — снятый с поддержки элемент управления ActiveX, созданный Microsoft с целью помочь разработчикам приложений в получении доступа к услугам, которые позволяют обеспечить безопасность для приложений на основе криптографических функций, реализованных в CryptoAPI, через технологию COM. CAPICOM можно использовать для цифровой подписи данных, проверки подписи, отображения информации о цифровой подписи и цифровом сертификате, добавлять или удалять сертификаты и, наконец, для шифрования и расшифровки данных.
http://cpdn.cryptopro.ru/content/cades/cadescom.html — здесь документация по усовершенстованному интерфейсу CADESCOM, на нее и будем опираться.
Ну а теперь код.
1) Для начала код контроллера и действия cms которые готовят HTML и Javascript код который получит клиент в своем браузере.
public function actionSign()
{
if (\Yii::$app->request->post())
{
$rr = new RequestResponse();
$fileName = \Yii::$app->request->post('fileName');
$signature = \Yii::$app->request->post('signature');
$cert = \Yii::$app->request->post('cert');
if ($signature && $fileName && $cert)
{
$fileName = ProjectComponent::translit($fileName);
$dirName = md5(time() . rand(1, 1000));
$rootDir = \Yii::getAlias('@frontend/web');
$dir = $rootDir . '/tmp/signs/' . $dirName;
$filePath = $dir . "/" . $fileName . ".sgn";
if (FileHelper::createDirectory($dir))
{
$file = fopen($filePath, "w+");
fwrite($file, $signature);
fclose($file);
}
$rr->data['src'] = \Yii::getAlias('@web/tmp/signs/' . $dirName . "/" . $fileName . ".sgn");
$rr->data['name'] = $fileName . ".sgn";
$rr->success = true;
$rr->message = '';
} else
{
$rr->success = false;
$rr->message = 'Недостаточно данных';
}
return $rr;
}
return $this->render($this->action->id);
}
2) View файл этого действия выглядит так:
<div class="row">
<div class="col-md-12">
<?= \frontend\widgets\CryptoProWidget::widget(); ?>
<div class="sx-content-wrapper">
<div style="display: none; text-align: center;" id="sx-process-crypto-info-global">
<? \yii\bootstrap\Alert::begin([
'options' =>
[
'class' => 'alert-info',
]
]); ?>
Исполнение может занять некоторое время. Ожидайте...
<? \yii\bootstrap\Alert::end(); ?>
</div>
<div class="sx-hidden-success">
<?
$model = new \yii\base\DynamicModel(['file']);
$form = \yii\bootstrap\ActiveForm::begin([
'action' => "/tools/sign-and-encrypt",
'id' => "sx-sign-form",
'options' => [
'enctype' => 'multipart/form-data'
]
]); ?>
<?= $form->field($model, 'file')->label("Файл для подписи")->fileInput([
'class' => 'sx-app-file'
]); ?>
<div style="display: none;">
<input type="hidden" name="sgn" id="sx-sgn-file"/>
<button type="submit">Отправить</button>
</div>
<? \yii\bootstrap\ActiveForm::end(); ?>
<div style="text-align: center">
<div style="display: none; text-align: center;" id="sx-process-crypto-info">
<? \yii\bootstrap\Alert::begin([
'options' =>
[
'class' => 'alert-info',
]
]); ?>
Исполнение может занять некоторое время. Ожидайте...
<? \yii\bootstrap\Alert::end(); ?>
</div>
<button class="btn btn-lg btn-primary sx-btn-submit" onclick="sx.FileApiSigner.execute(); return false;">Подписать</button>
</div>
</div>
<div style="display: none; text-align: center;" id="sx-result-wrapper-succss">
<? \yii\bootstrap\Alert::begin([
'options' =>
[
'class' => 'alert-success',
]
]); ?>
Вы можете скачать или просмотреть подпись, зашифровать файл и подпись
<? \yii\bootstrap\Alert::end(); ?>
<a href="#" class="btn btn-lg btn-primary sx-btn-download" target="_blank">Скачать подпись</a>
<a href="#" class="btn btn-lg btn-primary sx-btn-view" target="_blank">Посмотреть подпись</a>
<a href="#" class="btn btn-lg btn-primary sx-btn-encrypt" target="_blank">Зашифровать</a>
</div>
<div style="display: none;">
<div>
Файл подписи: <div id="sx-result-file">Тут будет результат</div>
</div>
<div>
<p id="info_msg" name="SignatureTitle">содержимое:</p>
<div id="item_border">
<textarea id="SignatureTxtBox" readonly style="font-size:9pt;height:100px;width:100%;resize:none;border:0;">
</textarea>
</div>
</div>
</div>
<div class="sx-show-success" style="display: none;">
<hr />
<a href="#" onclick="window.location.reload(); return false;" class="pull-right btn btn-default">
<i class="glyphicon glyphicon-repeat"></i>
Подисать еще один файл
</a>
</div>
</div>
</div>
</div>
3) Javascript реализующи основные взаимдействия с плагином и осуществляющий необходимые действия подписи файлов.
var DOWNLOAD_URL = "{$url}"; // константа задающая путь к серверному скрипту, через который будет происходить формирование файла для скачивания.
var VIEW_URL = "{$viewUrl}"; // константа задающая путь к серверному скрипту, через который будет происходить формирование файла для просмотра.
var CADESCOM_CADES_BES = 1; // константой задается формат подписи CAdES-BES (Basic Electronic Signature) в стандарте CAdES
var CAPICOM_CURRENT_USER_STORE = 2;
var CAPICOM_MY_STORE = "My"; // Хранилище персональных сертификатов пользователя.
var CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED = 2;
var CADESCOM_BASE64_TO_BINARY = 1;
var form = $('#sx-sign-form'); //Объект формы Jquery
//Функция отправки данных с формы
function trySubmit() {
try {
form[0].submit();
} catch (err) {
setTimeout(function () { trySubmit(); }, 50);
}
}
По клику на кнопку подписать отмеченную классом sx-btn-encrypt, происходит отправка формы
$('.sx-btn-encrypt').on('click', function()
{
$("#sx-process-crypto-info-global").show();
$('#sx-result-wrapper-succss').hide();
trySubmit();
return false;
});
(function(sx, $, _)
{
//Класс javascript создаваемый нами для удобства реализации сценария
sx.classes.FileApiSigner = sx.classes.Component.extend({
execute: function()
{
this.trigger('beforeSign');
var self = this;
// Проверяем, работает ли File API подробнее тут https://developer.mozilla.org/ru/docs/Web/API/FileReader
if (window.FileReader) {
// Браузер поддерживает File API.
} else {
this.trigger('error', {
'error': 'The File APIs are not fully supported in this browser.'
});
}
//Синтаксис javascript document.getElementById("dynamicmodel-file") - проверка в форме выбран ли файл для подписи
if (0 == document.getElementById("dynamicmodel-file").files.length) {
this.trigger('error', {
'error': "Выберите файл для подписи"
});
return;
}
//Дойдя до этого места, мы проверили, что браузер поддерживает работу с файлами и файл для подписи выбран
var oFile = document.getElementById("dynamicmodel-file").files[0];
var oFReader = new FileReader();
//Проверка наличия функции в библиотеке работы с файлами ниже подробнее про нужную нам функцию
if (typeof(oFReader.readAsDataURL)!="function") {
this.trigger('error', {
'error': "Method readAsDataURL() is not supported in FileReader."
});
return;
}
this.Certificate = sx.CryptoPro.Certificate;
if (!this.Certificate) {
this.trigger('error', {
'error': "Необходимо выбрать сертификат"
});
return;
}
// https://developer.mozilla.org/ru/docs/Web/API/FileReader/readAsDataURL
// Запускает процесс чтения данных указанного Blob, по завершении, аттрибут result будет содержать данные файла в виде data: URL.
oFReader.readAsDataURL(oFile);
// javascript имеет событийную модель, когда процесс чтения завершиться продолжаем исполнять сценарий
oFReader.onload = function(oFREvent) {
var header = ";base64,";
var sFileData = oFREvent.target.result; //аттрибут result будет содержать данные как URL, представляющий файл, кодированый в base64 строку.
var sBase64Data = sFileData.substr(sFileData.indexOf(header) + header.length); //Нужно убрать лишние заголовки, нам нужен только кодированные данные файла без заголовка.
// Вызов функции signCreate которая является функцией нашего же класса sx.classes.FileApiSigner для подписи данных
var signedMessage = self.signCreate(self.Certificate, sBase64Data);
// Выводим отделенную подпись в BASE64 на страницу
// Такая подпись должна проверяться в КриптоАРМ и cryptcp.exe
document.getElementById("SignatureTxtBox").innerHTML = signedMessage;
// На всякий случай дополнительная проверка подписи
var verifyResult = self.verify(signedMessage, sBase64Data);
if (verifyResult) {
self.trigger('signSuccess', {
'signature' : signedMessage,
'certObj' : new CertificateObj(self.Certificate),
'file' : oFile, //Файл для подписи
});
}
};
},
// Реализуем собственную функцию подписи данных, в ней 2 значения, сертификат и данные в формате base64
signCreate: function(oCertificate, dataToSign)
{
var oStore = cadesplugin.CreateObject("CAPICOM.Store");
oStore.Open(CAPICOM_CURRENT_USER_STORE, CAPICOM_MY_STORE,
CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED);
var oSigner = cadesplugin.CreateObject("CAdESCOM.CPSigner");
oSigner.Certificate = oCertificate;
var oSignedData = cadesplugin.CreateObject("CAdESCOM.CadesSignedData");
oSignedData.ContentEncoding = CADESCOM_BASE64_TO_BINARY;
oSignedData.Content = dataToSign;
try {
var sSignedMessage = oSignedData.SignCades(oSigner, CADESCOM_CADES_BES, true);
} catch (err) {
alert("Failed to create signature. Error: " + GetErrorMessage(err));
return;
}
oStore.Close();
return sSignedMessage;
},
verify: function(sSignedMessage, dataToVerify)
{
var oSignedData = cadesplugin.CreateObject("CAdESCOM.CadesSignedData");
try {
oSignedData.ContentEncoding = CADESCOM_BASE64_TO_BINARY;
oSignedData.Content = dataToVerify;
oSignedData.VerifyCades(sSignedMessage, CADESCOM_CADES_BES, true);
} catch (err) {
alert("Failed to verify signature. Error: " + GetErrorMessage(err));
return false;
}
return true;
}
});
sx.CryptoPro.bind('updateCertificate', function()
{
$('.sx-content-wrapper').show();
sx.FileApiSigner = new sx.classes.FileApiSigner();
sx.FileApiSigner.bind('error', function(e, data)
{
$("#sx-process-crypto-info").hide();
$(".sx-btn-submit").empty().append('Подписать').removeAttr('disabled');
sx.notify.error(data.error);
});
sx.FileApiSigner.bind('beforeSign', function(e, data)
{
$("#sx-process-crypto-info").show();
$(".sx-btn-submit").empty().append('Подождите').attr('disabled', 'disabled');
});
sx.FileApiSigner.bind('signSuccess', function(e, data)
{
$("#sx-process-crypto-info").hide();
$("#sx-result-file").empty().append('Идет формирование файла...');
$(".sx-btn-submit").empty().append('Подписать').removeAttr('disabled');
certObj = data.certObj;
var ajaxQuery = sx.ajax.preparePostQuery('', {
'signature' : data.signature,
'fileName' : data.file.name,
'cert' :
{
'certName' : certObj.GetCertName(),
'issuer' : certObj.GetIssuer(),
'certFromDate' : certObj.GetCertFromDate(),
'certTillDate' : certObj.GetCertTillDate(),
'pubKeyAlgorithm' : certObj.GetPubKeyAlgorithm(),
}
});
var ajaxHandler = new sx.classes.AjaxHandlerStandartRespose(ajaxQuery);
ajaxHandler.bind('success', function(e, response)
{
_.delay(function()
{
$("#sx-result-file").empty().append(
$("<a>", {
'href' : response.data.src,
//'target' : '_blank',
'download' : response.data.name
}).text(response.data.name)
);
$(".sx-btn-submit").remove();
var Container = $("#sx-result-wrapper-succss");
Container.show();
$('.sx-show-success').show();
$('.sx-hidden-success').hide();
$("#sx-sgn-file").val(response.data.src);
$(".sx-btn-download", Container).attr({
'href' : DOWNLOAD_URL + "?fileSrc=" + response.data.src
});
$(".sx-btn-view", Container).attr({
'href' : VIEW_URL + "?fileSrc=" + response.data.src
});
/*window.open(DOWNLOAD_URL + "?fileSrc=" + response.data.src);*/
}, 1000);
});
ajaxHandler.bind('error', function(e, data)
{
});
ajaxQuery.execute();
});
});
})(sx, sx.$, sx._);
Из кода выше следует подробно рассмотреть следующие функции, которые и осуществляют подпись и проверку подписи файлов.
var CADESCOM_CADES_BES = 1;
var CAPICOM_CURRENT_USER_STORE = 2;
var CAPICOM_MY_STORE = "My";
var CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED = 2;
var CADESCOM_BASE64_TO_BINARY = 1;
signCreate: function(oCertificate, dataToSign)
{
//Описывает хранилище сертификатов.
var oStore = cadesplugin.CreateObject("CAPICOM.Store");
//Opens a certificate store.
oStore.Open(CAPICOM_CURRENT_USER_STORE, CAPICOM_MY_STORE,
CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED);
//Объект, задающий параметры создания и содержащий информацию об усовершенствованной подписи.
var oSigner = cadesplugin.CreateObject("CAdESCOM.CPSigner");
oSigner.Certificate = oCertificate;
//Объект для работы с подписью данных
var oSignedData = cadesplugin.CreateObject("CAdESCOM.CadesSignedData");
oSignedData.ContentEncoding = CADESCOM_BASE64_TO_BINARY; //Кодировка
oSignedData.Content = dataToSign; //Данные для подписи
try {
//Метод SignCades позволяет добавить к сообщению усовершенствованную подпись.
//http://cpdn.cryptopro.ru/content/cades/interface_c_ad_e_s_c_o_m_1_1_i_c_p_signed_data2_4e6fce1eab3f028a937b412bdff182dd_14e6fce1eab3f028a937b412bdff182dd.html
var sSignedMessage = oSignedData.SignCades(oSigner, CADESCOM_CADES_BES, true);
} catch (err) {
alert("Failed to create signature. Error: " + GetErrorMessage(err));
return;
}
oStore.Close();
return sSignedMessage;
},
verify: function(sSignedMessage, dataToVerify)
{
var oSignedData = cadesplugin.CreateObject("CAdESCOM.CadesSignedData");
try {
oSignedData.ContentEncoding = CADESCOM_BASE64_TO_BINARY;
oSignedData.Content = dataToVerify;
oSignedData.VerifyCades(sSignedMessage, CADESCOM_CADES_BES, true);
} catch (err) {
alert("Failed to verify signature. Error: " + GetErrorMessage(err));
return false;
}
return true;
}
По этому же принципу работают все остальные криптографические функции, поэтому ниже рассмотрим лишь конкретные примеры.
Вычисление хэш значения
var CADESCOM_HASH_ALGORITHM_CP_GOST_3411 = 100;
function run() {
// Создаем объект CAdESCOM.HashedData
var oHashedData = cadesplugin.CreateObject("CAdESCOM.HashedData");
// Алгоритм хэширования нужно указать до того, как будут переданы данные
oHashedData.Algorithm = CADESCOM_HASH_ALGORITHM_CP_GOST_3411;
// Передаем данные
oHashedData.Hash("Some data here.");
// Вычисляем хэш-значение
var sHashValue1 = oHashedData.Value;
// Хэш-значение будет вычислено от данных в кодировке UCS2-LE
// Для алгоритма SHA-1 хэш-значение будет совпадать с вычисленным при помощи CAPICOM
document.getElementById("hashVal1").innerHTML = sHashValue1;
// Получение значения свойства oHashedData.Value сбрасывает
// состояние объекта (алгоритм хэширования остается прежним).
// Но само значение свойства можно получить несколько раз:
var sHashValue2 = oHashedData.Value;
document.getElementById("hashVal2").innerHTML = sHashValue2;
// То же самое хэш-значение можно получить, если передать данные по частям
oHashedData.Hash("Some ");
oHashedData.Hash("data ");
oHashedData.Hash("here.");
var sHashValue3 = oHashedData.Value;
document.getElementById("hashVal3").innerHTML = sHashValue3;
}
Вычисление хэш значения бинарных данных (его и используем в проекте)
var CADESCOM_HASH_ALGORITHM_CP_GOST_3411 = 100;
var CADESCOM_BASE64_TO_BINARY = 1;
function run() {
// Создаем объект CAdESCOM.HashedData
var oHashedData = cadesplugin.CreateObject("CAdESCOM.HashedData");
// Алгоритм хэширования нужно указать до того, как будут переданы данные
oHashedData.Algorithm = CADESCOM_HASH_ALGORITHM_CP_GOST_3411;
// Указываем кодировку данных
// Кодировка должна быть указана до того, как будут переданы сами данные
oHashedData.DataEncoding = CADESCOM_BASE64_TO_BINARY;
// Предварительно закодированные в BASE64 бинарные данные
// В данном случае закодирован файл со строкой "Some Data."
var dataInBase64 = "U29tZSBEYXRhLg==";
// Передаем данные
oHashedData.Hash(dataInBase64);
// Получаем хэш-значение
var sHashValue = oHashedData.Value;
// Это значение будет совпадать с вычисленным при помощи, например,
// утилиты cryptcp от тех же исходных _бинарных_ данных.
// В данном случае - от файла со строкой "Some Data."
document.getElementById("hashVal").innerHTML = sHashValue;
}
Шифроваение данных
/**
* Функция шифрования бинарных данных
* @param oCertificate выбранный сертификат
* @param data бинарные данные закодированные в base64 строку
*/
encrypt: function(oCertificate, data)
{
var self = this;
//Синтаксис языка javascript проверки ошибок, если не удастся создать объект cadescom.CPEnvelopedData
try {
// Создаем объект cadescom.CPEnvelopedData
var oEnvelop = cadesplugin.CreateObject("cadescom.CPEnvelopedData");
} catch (err) {
alert('Failed to create CAdESCOM.CPEnvelopedData: ' + err.number);
return;
}
// Указываем кодировку данных
// Кодировка должна быть указана до того, как будут переданы сами данные
oEnvelop.ContentEncoding = CADESCOM_BASE64_TO_BINARY;
// Предварительно закодированные в BASE64 бинарные данные
oEnvelop.Content = data;
//Установка сертификата для шифрования
oEnvelop.Recipients.Add(self.Certificate);
//Получаем зашифрованные данные
return oEnvelop.Encrypt();
},
Расшифровка данных
/**
* Функция расшифровки бинарных данных
* @param EncryptedData шифр
*/
encrypt: function(EncryptedData)
{
var self = this;
//Синтаксис языка javascript проверки ошибок, если не удастся создать объект cadescom.CPEnvelopedData
try {
// Создаем объект cadescom.CPEnvelopedData
var oEnvelop = cadesplugin.CreateObject("cadescom.CPEnvelopedData");
} catch (err) {
alert('Failed to create CAdESCOM.CPEnvelopedData: ' + err.number);
return;
}
// Указываем кодировку данных
// Кодировка должна быть указана до того, как будут переданы сами данные
oEnvelop.ContentEncoding = CADESCOM_BASE64_TO_BINARY;
// Запуск процесса расшифровки данных
oEnvelop.Decrypt(EncryptedData);
//В результате расшифровки объект oEnvelop заполнит свойство Content - которое и будет содержать расшифрованные данные
var sDecriptedData = oEnvelop.Content;
return sDecriptedData;
},