В данном обзоре рассмотрим процесс создания веб-инструмента, который позволит нам подписывать различные файлы электронной подписью.

Веб-инструмент — это сайт. Поэтому нам потребуется сервер, с необходимым ПО и какая то система управления им.

Наш сайт будет написан на языке php.


И так, в качестве серверного ПО будем использовать:

На этом не стоит подробно останавливаться, потому что, бэкенда часть нашего сервиса выполняет минимальные действия (формирование и отдача html страниц).

Поэтому по правилам SkeekS CMS мы сформируем html страницу, на которой будет присутствовать html+css+js код.


Сформированная сайтом страница рендерится в браузере клиента. в момент рендеринга, исполняется js и css код присутствующий на ней.

JavaScript (/ˈɑː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;
},

 

Все комментарии (4)
Ягодкин Дмитрий
Class 'frontend\widgets\CryptoProWidget' not found где взять? :)
Семенов Александр
Этот виджет написан для одного из наших проектов. Чуть позже могу поделиться кодом.
id177
Александр, на сколько я знаю NPAPI уже не поддерживается в современных браузерах. Объект CPEnvelopedData требует ActivX для создания. Как вы реализуете поддержку других браузеров (отличных от IE) и других операционных систем (в т.ч. мобильных)?
Семенов Александр
В настоящее время не занимаемся этим направлением. Поэтому не реализуем.