?>

La implementación de la herramienta electrónica de firma de archivos mediante el complemento CryptoPro EDS Browser

La implementación de la herramienta electrónica de firma de archivos mediante el complemento CryptoPro EDS Browser

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

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

Наш сайт будет написан на языке 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;
},

 

todos los comentarios (5)
Дмитрий Ягодкин
Class 'frontend\widgets\CryptoProWidget' not found
где взять? :)
Этот виджет написан для одного из наших проектов. Чуть позже могу поделиться кодом.
s-klaus@mail.ru
Александр, на сколько я знаю NPAPI уже не поддерживается в современных браузерах. Объект CPEnvelopedData требует ActivX для создания. Как вы реализуете поддержку других браузеров (отличных от IE) и других операционных систем (в т.ч. мобильных)?
В настоящее время не занимаемся этим направлением. Поэтому не реализуем.
liebe.amore@gmail.com
Александр, есть необходимость перенести ваш код на другую CMF MODX Revolution. Может быть вы могли бы взяться? Если заинтересует - пишите в skype: liebe.amore.