Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

Опыт применения технологии рутокен для регистрации и авторизации пользователей в системе (часть 4)

Добрый день!

Итак, мы разобрались, каким образом будет происходить аутентификация пользователей в системе, а так же создали свой локальный удостоверяющий центр с корневым приватным ключом и корневым сертификатом. Теперь пора перейти к работе непосредственно с Рутокеном. Напомню, что работать мы будем с Рутокеном из клиентской части приложения через Рутокен плагин.

Надеюсь, что драйвера для Рутокена и Рутокен плагин уже установлены на вашем компьютере. Если нет, то предлагаю еще раз ознакомиться с первой частью статьи.

Общая документация, как работать с Рутокен плагин находится по ссылкам:
Встраивание Рутокен ЭЦП 2.0 через Рутокен Плагин
Руководство Разработчика Версия 4.4.1

Если вы начнете читать документацию, то у вас может возникнуть вопрос (по крайней мере так было у меня). Что за объект такой plugin? Где его взять, чтобы потом применять к нему всякого рода функции из документации. Разбираясь с данной проблемой, я совершенно случайно попал на эту страницу

Там есть пример кода, как раз из которого у меня и удалось получить этот самый объект.
Тут приведу код, который тупо надо скопировать к себе, после чего вы сможете работать с документацией:

Открыть код

let rutoken = (function (my) {

    let loadCallbacks = [];
    let pluginMimeType = "application/x-rutoken-pki";
    let extension = window["C3B7563B-BF85-45B7-88FC-7CFF1BD3C2DB"];

    function isFunction(obj) {
        return !!(obj && obj.call && obj.apply);
    }

    function proxyMember(target, member) {
        if (isFunction(target[member])) {
            return function () {
                return target[member].apply(target, arguments);
            };
        } else {
            return target[member];
        }
    }

    function returnPromise(promise) {
        return function () {
            return promise;
        };
    }

    function initialize() {
        my.ready = Promise.resolve(true);
        my.isExtensionInstalled = returnPromise(Promise.resolve(false));
        my.isPluginInstalled = returnPromise(Promise.resolve(true));
        my.loadPlugin = loadPlugin;
        window.rutokenLoaded = onPluginLoaded;
    }

    function initializeExtension() {
        let readyPromise = extension.initialize().then(function () {
            return extension.isPluginInstalled();
        }).then(function (result) {
            my.isExtensionInstalled = returnPromise(Promise.resolve(true));
            my.isPluginInstalled = proxyMember(extension, "isPluginInstalled");

            if (result) {
                pluginMimeType = "application/x-rutoken-plugin";
                my.loadPlugin = loadChromePlugin;
            }

            return true;
        });

        my.ready = readyPromise;
    }

    function initializeWithoutPlugin() {
        my.ready = Promise.resolve(true);
        my.isExtensionInstalled = returnPromise(Promise.resolve(false));
        my.isPluginInstalled = returnPromise(Promise.resolve(false));
    }

    function loadPlugin() {
        let obj = document.createElement("object");
        obj.style.setProperty("visibility", "hidden", "important");
        obj.style.setProperty("width", "0px", "important");
        obj.style.setProperty("height", "0px", "important");
        obj.style.setProperty("margin", "0px", "important");
        obj.style.setProperty("padding", "0px", "important");
        obj.style.setProperty("border-style", "none", "important");
        obj.style.setProperty("border-width", "0px", "important");
        obj.style.setProperty("max-width", "0px", "important");
        obj.style.setProperty("max-height", "0px", "important");

        // onload callback must be set before type attribute in IE earlier than 11.
        obj.innerHTML = "<param name='onload' value='rutokenLoaded'/>";
        // Just after setting type attribute before function returns promise
        // FireBreath uses onload callback to execute it with a small delay.
        // So it must be valid, but it will be called a little bit later.
        // In other browsers plugin will be initialized only after appending
        // an element to the document.
        obj.setAttribute("type", pluginMimeType);

        document.body.appendChild(obj);

        let promise = new Promise(function (resolve, reject) {
            loadCallbacks.push(resolve);
        });

        return promise;
    }

    function loadChromePlugin() {
        return extension.loadPlugin().then(function (plugin) {
            return resolveObject(plugin);
        }).then(function (resolvedPlugin) {
            resolvedPlugin.wrapWithOldInterface = wrapNewPluginWithOldInterface;
            return resolvedPlugin;
        });
    }

    function onPluginLoaded(plugin, error) {
        wrapOldPluginWithNewInterface(plugin).then(function (wrappedPlugin) {
            if (loadCallbacks.length == 0) {
                throw "Internal error";
            }

            let callback = loadCallbacks.shift();
            callback(wrappedPlugin);
        });
    }

    function resolveObject(obj) {
        let resolvedObject = {};
        let promises = [];

        for (var m in obj) {
            (function (m) {
                if (isFunction(obj[m].then)) {
                    promises.push(obj[m].then(function (result) {
                        return resolveObject(result).then(function (resolvedProperty) {
                            if (isFunction(resolvedProperty)) {
                                resolvedObject[m] = proxyMember(obj, m);
                            } else {
                                resolvedObject[m] = resolvedProperty;
                            }
                        });
                    }));
                } else {
                    resolvedObject[m] = obj[m];
                }
            })(m);
        }

        if (promises.length == 0) {
            return new Promise(function (resolve) {
                resolve(obj);
            });
        } else {
            return Promise.all(promises).then(function () {
                return resolvedObject;
            });
        }
    }

    function wrapNewPluginWithOldInterface() {
        let wrappedPlugin = {};

        for (var m in this) {
            if (isFunction(this[m])) {
                wrappedPlugin[m] = (function (plugin, member) {
                    return function () {
                        var successCallback = arguments[arguments.length - 2];
                        var errorCallback = arguments[arguments.length - 1];
                        var args = Array.prototype.slice.call(arguments, 0, -2);
                        return member.apply(plugin, args).then(function (result) {
                            successCallback(result);
                        }, function (error) {
                            errorCallback(error.message);
                        });
                    };
                })(this, this[m]);
            } else {
                wrappedPlugin[m] = this[m];
            }
        }

        return new Promise(function (resolve) {
            resolve(wrappedPlugin);
        });
    }

    function wrapOldPluginWithOldInterface() {
        let unwrappedPlugin = {originalObject: this.originalObject};

        for (let m in this.originalObject) {
            unwrappedPlugin[m] = proxyMember(this.originalObject, m);
        }

        return new Promise(function (resolve) {
            resolve(unwrappedPlugin);
        });
    }

    function wrapOldPluginWithNewInterface(plugin) {
        let wrappedPlugin = {
            originalObject: plugin,
            wrapWithOldInterface: wrapOldPluginWithOldInterface
        };

        for (let m in plugin) {
            if (isFunction(plugin[m])) {
                wrappedPlugin[m] = (function (plugin, member) {
                    return function () {
                        let args = Array.prototype.slice.call(arguments);
                        return new Promise(function (resolve, reject) {
                            args.push(resolve, reject);
                            member.apply(plugin, args);
                        });
                    };
                })(plugin, plugin[m]);
            } else {
                wrappedPlugin[m] = plugin[m];
            }
        }

        return new Promise(function (resolve) {
            resolve(wrappedPlugin);
        });
    }

    if (extension) {
        initializeExtension();
    } else if (navigator.mimeTypes && navigator.mimeTypes[pluginMimeType]) {
        initialize();
    } else {
        try {
            let plugin = new ActiveXObject("Aktiv.CryptoPlugin");
            initialize();
        } catch (e) {
            initializeWithoutPlugin();
        }
    }

    return my;
}({}));
rutoken.ready
    // Проверка установки расширение 'Адаптера Рутокен Плагина' в Google Chrome
    .then(function () {
        if (window.chrome || typeof InstallTrigger !== 'undefined') {
            return rutoken.isExtensionInstalled();
        } else {
            console.log("расширение 'Адаптер Рутокен Плагина' не найдено. Установите адаптер рутокена в браузер");
            return Promise.resolve(true);
        }
    })
    // Проверка установки Рутокен Плагина
    .then(function (result) {
        if (result) {
            console.log("расширение 'Адаптер Рутокен Плагина' найдено");
            return rutoken.isPluginInstalled();
        } else {
            return Promise.reject("Не удаётся найти расширение 'Адаптер Рутокен Плагина'");
        }
    })
    // Загрузка плагина
    .then(function (result) {
        if (result) {
            console.log("Рутокен плагин найден");
            return rutoken.loadPlugin();
        } else {
            return Promise.reject("Не удаётся найти Плагин");
        }
    })
    //Можно начинать работать с плагином
    .then(function (plugin_) {
        plugin = plugin_;
        if (!plugin) {
            console.log("Не удаётся загрузить Рутокен Плагин");
            return Promise.reject("Не удаётся загрузить Плагин");
        } else {
            console.log("Рутокен плагин загружен успешно");
            return plugin.enumerateDevices()
        }
    })

Похожее:  4. UserGate Getting Started. Работа с пользователями / Хабр

Чтобы успешно использовать Рутокен плагин и вообще понимать, как работает этот код, нужно разобраться, что такое Promise. Есть

отличная статья

на эту тему, в которой имеется все что нужно для понимания Promise.

Напомню, что для нашей задачи необходимо сначала создать ключевую пару на Рутокене, а затем сформировать запрос на сертификат, который после этого будет отправлен на сервер для получения сертификата.

Для написания своих клиентский приложения я использую фреймворк Ext Js, но это абсолютно не важно. Уверен, что понять весь этот код можно и без знания Ext Js.

Перейдем к реализации. Для начала нужно получить соединение с Рутокеном. Для этого был создан класс, который будет за это отвечать:

Для работы с этим классом просто нужно создать его объект, а потом вызвать функцию connectToDevice. В результате эта функция возвращает список, состоящий из плагина и номера первого Рутокена. Функция connectToDevice принимает параметр rutoken. Этот объект получается копированием части содержимого кода, приведенного в начале статьи:

Открыть реализацию Rutoken.rutoken.RutokenInit

Ext.define("Rutoken.rutoken.RutokenInit", {

    config: {
        rutoken: undefined,
    },

    constructor: function(){
        let rutoken = (function (my) {

            let loadCallbacks = [];
            let pluginMimeType = "application/x-rutoken-pki";
            let extension = window["C3B7563B-BF85-45B7-88FC-7CFF1BD3C2DB"];

            function isFunction(obj) {
                return !!(obj && obj.call && obj.apply);
            }

            function proxyMember(target, member) {
                if (isFunction(target[member])) {
                    return function () {
                        return target[member].apply(target, arguments);
                    };
                } else {
                    return target[member];
                }
            }

            function returnPromise(promise) {
                return function () {
                    return promise;
                };
            }

            function initialize() {
                my.ready = Promise.resolve(true);
                my.isExtensionInstalled = returnPromise(Promise.resolve(false));
                my.isPluginInstalled = returnPromise(Promise.resolve(true));
                my.loadPlugin = loadPlugin;
                window.rutokenLoaded = onPluginLoaded;
            }

            function initializeExtension() {
                let readyPromise = extension.initialize().then(function () {
                    return extension.isPluginInstalled();
                }).then(function (result) {
                    my.isExtensionInstalled = returnPromise(Promise.resolve(true));
                    my.isPluginInstalled = proxyMember(extension, "isPluginInstalled");

                    if (result) {
                        pluginMimeType = "application/x-rutoken-plugin";
                        my.loadPlugin = loadChromePlugin;
                    }

                    return true;
                });

                my.ready = readyPromise;
            }

            function initializeWithoutPlugin() {
                my.ready = Promise.resolve(true);
                my.isExtensionInstalled = returnPromise(Promise.resolve(false));
                my.isPluginInstalled = returnPromise(Promise.resolve(false));
            }

            function loadPlugin() {
                let obj = document.createElement("object");
                obj.style.setProperty("visibility", "hidden", "important");
                obj.style.setProperty("width", "0px", "important");
                obj.style.setProperty("height", "0px", "important");
                obj.style.setProperty("margin", "0px", "important");
                obj.style.setProperty("padding", "0px", "important");
                obj.style.setProperty("border-style", "none", "important");
                obj.style.setProperty("border-width", "0px", "important");
                obj.style.setProperty("max-width", "0px", "important");
                obj.style.setProperty("max-height", "0px", "important");

                // onload callback must be set before type attribute in IE earlier than 11.
                obj.innerHTML = "<param name='onload' value='rutokenLoaded'/>";
                // Just after setting type attribute before function returns promise
                // FireBreath uses onload callback to execute it with a small delay.
                // So it must be valid, but it will be called a little bit later.
                // In other browsers plugin will be initialized only after appending
                // an element to the document.
                obj.setAttribute("type", pluginMimeType);

                document.body.appendChild(obj);

                let promise = new Promise(function (resolve, reject) {
                    loadCallbacks.push(resolve);
                });

                return promise;
            }

            function loadChromePlugin() {
                return extension.loadPlugin().then(function (plugin) {
                    return resolveObject(plugin);
                }).then(function (resolvedPlugin) {
                    resolvedPlugin.wrapWithOldInterface = wrapNewPluginWithOldInterface;
                    return resolvedPlugin;
                });
            }

            function onPluginLoaded(plugin, error) {
                wrapOldPluginWithNewInterface(plugin).then(function (wrappedPlugin) {
                    if (loadCallbacks.length == 0) {
                        throw "Internal error";
                    }

                    let callback = loadCallbacks.shift();
                    callback(wrappedPlugin);
                });
            }

            function resolveObject(obj) {
                let resolvedObject = {};
                let promises = [];

                for (var m in obj) {
                    (function (m) {
                        if (isFunction(obj[m].then)) {
                            promises.push(obj[m].then(function (result) {
                                return resolveObject(result).then(function (resolvedProperty) {
                                    if (isFunction(resolvedProperty)) {
                                        resolvedObject[m] = proxyMember(obj, m);
                                    } else {
                                        resolvedObject[m] = resolvedProperty;
                                    }
                                });
                            }));
                        } else {
                            resolvedObject[m] = obj[m];
                        }
                    })(m);
                }

                if (promises.length == 0) {
                    return new Promise(function (resolve) {
                        resolve(obj);
                    });
                } else {
                    return Promise.all(promises).then(function () {
                        return resolvedObject;
                    });
                }
            }

            function wrapNewPluginWithOldInterface() {
                let wrappedPlugin = {};

                for (var m in this) {
                    if (isFunction(this[m])) {
                        wrappedPlugin[m] = (function (plugin, member) {
                            return function () {
                                var successCallback = arguments[arguments.length - 2];
                                var errorCallback = arguments[arguments.length - 1];
                                var args = Array.prototype.slice.call(arguments, 0, -2);
                                return member.apply(plugin, args).then(function (result) {
                                    successCallback(result);
                                }, function (error) {
                                    errorCallback(error.message);
                                });
                            };
                        })(this, this[m]);
                    } else {
                        wrappedPlugin[m] = this[m];
                    }
                }

                return new Promise(function (resolve) {
                    resolve(wrappedPlugin);
                });
            }

            function wrapOldPluginWithOldInterface() {
                let unwrappedPlugin = {originalObject: this.originalObject};

                for (let m in this.originalObject) {
                    unwrappedPlugin[m] = proxyMember(this.originalObject, m);
                }

                return new Promise(function (resolve) {
                    resolve(unwrappedPlugin);
                });
            }

            function wrapOldPluginWithNewInterface(plugin) {
                let wrappedPlugin = {
                    originalObject: plugin,
                    wrapWithOldInterface: wrapOldPluginWithOldInterface
                };

                for (let m in plugin) {
                    if (isFunction(plugin[m])) {
                        wrappedPlugin[m] = (function (plugin, member) {
                            return function () {
                                let args = Array.prototype.slice.call(arguments);
                                return new Promise(function (resolve, reject) {
                                    args.push(resolve, reject);
                                    member.apply(plugin, args);
                                });
                            };
                        })(plugin, plugin[m]);
                    } else {
                        wrappedPlugin[m] = plugin[m];
                    }
                }

                return new Promise(function (resolve) {
                    resolve(wrappedPlugin);
                });
            }

            if (extension) {
                initializeExtension();
            } else if (navigator.mimeTypes && navigator.mimeTypes[pluginMimeType]) {
                initialize();
            } else {
                try {
                    let plugin = new ActiveXObject("Aktiv.CryptoPlugin");
                    initialize();
                } catch (e) {
                    initializeWithoutPlugin();
                }
            }

            return my;
        }({}));
        this.setRutoken(rutoken);
    }

});

Таким образом, нужно создать объект данного класса и подставить его в функцию connectToDevice класса Rutoken.rutoken.ConnectToDevice, код которого был приведен ранее. Это действие я делаю в следующем классе, который отвечает за регистрацию пользователя:

Открыть реализацию Rutoken.rutoken.RutokenRegistration

Ext.define('Rutoken.rutoken.RutokenRegistration', {

    registration: function (rutoken, device, scope) {
        return device.connectToDevice(rutoken)
            //Получаем исходные данные для проведения регистрации
            .then(function (pluginWithDeviceIndex) {
                values = scope.lookupReference('regForm').getValues();
                plugin = pluginWithDeviceIndex[0];
                deviceIndex = pluginWithDeviceIndex[1];
                host = 'http://localhost:8080/';
                createCrtDataPrefix = 'createCrtData';
                registrationPrefix = 'registration';
            })
            //Получаем список сертификатов с рутокена
            .then(function () {
                return plugin.enumerateCertificates(deviceIndex, plugin.CERT_CATEGORY_USER);
            })
            //Проверяем существование ключевой пары на рутокене
            .then(function (crts) {
                if (crts.length > 0) {
                    throw "Certificate already exist on rutoken";
                }
            })
            //Создаем ключевую пару на рутокене по GOST3410_2022_256
            .then(function () {
                let option = {
                    "publicKeyAlgorithm": plugin.PUBLIC_KEY_ALGORITHM_GOST3410_2022_256
                };
                return plugin.generateKeyPair(deviceIndex, undefined, "", option);
            })
            //Формируем запрос createPkcs10 на выдачу сертификата
            .then(function (keyPair) {
                let subject = [
                    {
                        rdn: "countryName",
                        value: "RU"
                    }
                    , {
                        rdn: "stateOrProvinceName",
                        value: "Russia"
                    }
                    , {
                        rdn: "localityName",
                        value: "Saint-Petersburg"
                    }
                    , {
                        rdn: "streetAddress",
                        value: "street"
                    }
                    , {
                        rdn: "organizationName",
                        value: "Eurica"
                    }
                    , {
                        rdn: "organizationalUnitName",
                        value: "Rutoken"
                    }
                    , {
                        rdn: "title",
                        value: "инженер"
                    }
                    , {
                        rdn: "commonName",
                        value: `${values.name} ${values.surname}`
                    }
                    , {
                        rdn: "postalAddress",
                        value: "postal address"
                    }
                    , {
                        rdn: "pseudonym",
                        value: "инженер123"
                    }
                    , {
                        rdn: "surname",
                        value: `${values.surname}`
                    }
                    , {
                        rdn: "givenName",
                        value: "given name"
                    }
                    , {
                        rdn: "emailAddress",
                        value: `${values.email}`
                    }
                ];
                let keyUsageVal = [
                    "digitalSignature"
                    , "nonRepudiation"
                    , "keyEncipherment"
                    , "dataEncipherment"
                    , "keyAgreement"
                    , "keyCertSign"
                    , "cRLSign"
                    , "encipherOnly"
                    , "decipherOnly"
                ];
                let extKeyUsageVal = [
                    "emailProtection"
                    , "clientAuth"
                    , "serverAuth"
                    , "codeSigning"
                    , "timeStamping"
                    , "msCodeInd"
                    , "msCodeCom"
                    , "msCTLSign"
                    , "1.3.6.1.5.5.7.3.9" // OSCP
                    , "1.2.643.2.2.34.6" // CryptoPro RA user

                ];
                let certificatePolicies = [
                    "1.2.643.100.113.1", // КС1
                    "1.2.643.100.113.2", // КС2
                    "1.2.643.100.113.3", // КС3
                    "1.2.643.100.113.4", // КВ1
                    "1.2.643.100.113.5", // КВ2
                    "1.2.643.100.113.6"  // КА1
                ];
                let extensions = {
                    "keyUsage": keyUsageVal,
                    "extKeyUsage": extKeyUsageVal,
                    "certificatePolicies": certificatePolicies
                };
                let options = {
                    "subjectSignTool": 'СКЗИ "РУТОКЕН ЭЦП"',
                    "hashAlgorithm": plugin.HASH_TYPE_GOST3411_12_256,
                    "customExtensions": [
                        {
                            oid: "1.3.6.1.4.1.311.21.7",
                            value: "MA0GCCqFAwICLgAIAgEB",
                            criticality: false
                        }
                    ],
                };
                return plugin.createPkcs10(deviceIndex, keyPair, subject, extensions, options);
            })
            //создаем объект pkcs10Message для отправки на сервер
            .then(function (pkcs10Request) {
                let pkcs10Message = Ext.create('Rutoken.model.RuTokenPkcs10Message');
                pkcs10Message.requestPkcs10Message = pkcs10Request;
                return pkcs10Message;
            })
            //отправляем pkcs10Message на сервер для формирования сертификата
            .then(function (pkcs10Message) {
                return new Promise(function (resolve, reject) {
                    Ext.Ajax.request({
                        method: 'POST',
                        jsonData: Ext.encode(pkcs10Message),
                        url: `${host}${createCrtDataPrefix}`,
                        scope: this,
                        headers: {
                            'accept': 'application/json',
                        },
                        success: function (response) {
                            let crt = Ext.decode(response.responseText);
                            plugin.importCertificate(deviceIndex, crt.responseCertificateMessage, plugin.CERT_CATEGORY_USER);
                            console.log('Certificate was saved successful');
                            resolve(crt);
                        },
                        failures: function (response) {
                            console.log('Failure Pkcs10 request');
                            reject(response.status);
                        }
                    });
                });
            });
    }
});

В качестве параметров функция registration получает объекты классов Rutoken.rutoken.RutokenInit (rutoken), Rutoken.rutoken.ConnectToDevice (device), третьим параметром передается scope на ViewController, чтобы у меня была возможность работать с данными с формы (рассмотрение данной части выходит за рамки статьи).

Считаю, что код имеет достаточное количество комментариев, чтобы можно было понять, как он работает. Обращу внимание только на некоторые моменты.

Во-первых, обратите внимание на часть кода помеченную комментарием «Формируем запрос createPkcs10 на выдачу сертификата». Рассмотрим переменную subject. Она представляет собой ассоциативный массив, который содержит в себе данные пользователя. Эти данные потом будут отражаться в самом сертификате. Дело в том, что по умолчанию можно менять только значения этого массива, но нельзя добавлять новые или удалять старые элементы этого массива. Обратите на это внимание, иначе ваша функция для создания запроса работать не будет.

Во-вторых, хочу обратить внимание на то, что алгоритм, с помощью которого формируется ключевая пара (в моем случае PUBLIC_KEY_ALGORITHM_GOST3410_2022_256), должен соответствовать алгоритму хеширования в переменной options (в моем случае это «hashAlgorithm»: plugin.HASH_TYPE_GOST3411_12_256). Если будете использовать другой алгоритм создания ключевой пары, то алгоритм хеширования должен быть соответствующий. В противном случае ваша функция для создания запроса также работать не будет.

После формирования запроса на создание сертификата, происходит отправка этого запроса на сервер, и в ответ приходит сам сертификат, который успешно импортируется на Рутокен.

Отлично! Мы произвели регистрацию пользователя в системе. Теперь мы имеем на своем Рутокене сертификат с неэкспортируемой ключевой парой, а также удостоверяющий центр знает о сертификате, который записан на Рутокене, так как он сам его выдал с помощью первой команды, описанной в данной статье.

Перейдем к авторизации пользователя. Алгоритм процесса авторизации описан тут.

Для программного описания процесса авторизации мной был создан класс Rutoken.rutoken.RutokenAuthorization:

Открыть реализацию Rutoken.rutoken.RutokenAuthorization


Ext.define('Rutoken.rutoken.RutokenAuthorization', {

    reference: "ruTokenAuthorization",

    authorization: function (rutoken, device, scope) {
        return device.connectToDevice(rutoken)
            //Получаем исходные данные для проведения аутентификации
            .then(function (pluginWithDeviceIndex) {
                plugin = pluginWithDeviceIndex[0];
                deviceIndex = pluginWithDeviceIndex[1];
                values = scope.lookupReference('authForm').getValues();
                host = 'http://localhost:8080/';
                prefixSalt = 'get-authentication-salt';
                prefixAuthentication = 'authentication';
                prefixLogin = 'login';
            })
            //Делаем запрос на аутентификацию на сервер и получаем случайную строку salt
            .then(function () {
                return new Promise(function (resolve, reject) {
                    Ext.Ajax.request({
                        method: 'POST',
                        jsonData: "",
                        url: `${host}${prefixSalt}`,
                        scope: this,
                        headers: {
                            'accept': 'application/json',
                        },
                        success: function (response) {
                            console.log(`Success ${host}${prefixSalt} request`);
                            resolve(Ext.decode(response.responseText));
                        },
                        failures: function (response) {
                            console.log(`Failure ${host}${prefixSalt} request`);
                            reject(response.status);
                        }
                    });
                });
            })
            .then(function (responseData) {
                salt = responseData;
            })
            //Получаем список сертификатов с рутокена
            .then(function () {
                return plugin.enumerateCertificates(deviceIndex, plugin.CERT_CATEGORY_USER);
            })
            //Формируем запрос на аутентификацию. Для этого используем только первый сертификат.
            //На устройстве должен быть только один сертификат привязанный к ключевой паре.
            .then(function (crts) {
                return plugin.authenticate(deviceIndex, crts[0], salt.salt);
            })
            //Корректируем запрос на аутентификацию для правильного понимания запроса сервером
            .then(function (auth) {
                auth = "-----BEGIN CMS-----n"   auth   "-----END CMS-----";
                return auth;
            })
            //Формирум объект authenticateMessage для отправки его на сервер
            .then(function (auth) {
                let authenticateMessage = Ext.create("Rutoken.model.RuTokenAuthenticateMessage");
                authenticateMessage.authenticateMessage = auth;
                return authenticateMessage;
            })
            //Отправляем запрос authenticateMessage на сервер
            .then(function (authenticateMessage) {
                return new Promise(function (resolve, reject) {
                    Ext.Ajax.request({
                        method: 'POST',
                        jsonData: Ext.encode(authenticateMessage),
                        url: `${host}${prefixAuthentication}`,
                        scope: this,
                        headers: {
                            'accept': 'application/json',
                        },
                        success: function (response) {
                            console.log(`Success ${host}${prefixAuthentication} request`);
                            resolve(Ext.decode(response.responseText).authorizationSuccess);
                        },
                        failures: function (response) {
                            console.log(`Failure ${host}${prefixAuthentication} request`);
                            reject(response.status);
                        }
                    });
                });
            });
    },
});

В результате мы получим объект authorizationSuccess с логическим значением, которое говорит о том, что прошла ли наша авторизация успешно или нет.

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

Надеюсь, этот цикл статей был полезен для вас, желаю удачи и спасибо за внимание!

Работа с эцп на портале «электронного правительства»

Много различных мнений вызывают услуги в формате онлайн: кто-то относится с недоверием, кому-то это кажется слишком сложным, а кто-то уже давно и успешно пользуется электронными услугами, экономя свое время, деньги и силы. И первый шаг на пути к продуктивной организации своего времени, бизнеса и даже жизни – получение ключей ЭЦП. Что скрывается под этими магическими буквами, и как их использовать мы расскажем в этом посте.

Что такое ЭЦП?

Электронно-цифровая подпись (ЭЦП) – это аналог собственноручной подписи, который используется для придания электронному документу такой же юридической силы, как если бы этот документ был на бумажном носителе с подписью и скрепленной печатью.

ЭЦП является реквизитом электронного документа, полученного в результате криптографического преобразования информации с использованием электронного регистрационного свидетельства (далее по тексту – Сертификата) и закрытого ключа ЭЦП.

Проще говоря, использование ЭЦП – это полноценная замена собственноручной подписи.

Согласно Закону Республики Казахстан от 7 января 2003 года «Об электронном документе и электронной цифровой подписи» дано понятие «регистрационное свидетельство», которое в международной практике используется как «сертификат» или «сертификат открытого ключа».
Основные понятия, взятые с вышеуказанного закона

  • Национальный удостоверяющий центр Республики Казахстан – удостоверяющий центр, обслуживающий участников “электронного правительства”, государственных и негосударственных информационных систем;
  • регистрационное свидетельство – документ на бумажном носителе или электронный документ, выдаваемый удостоверяющим центром для подтверждения соответствия электронной цифровой подписи требованиям, установленным настоящим Законом;
  • владелец регистрационного свидетельства – физическое или юридическое лицо, на имя которого выдано регистрационное свидетельство, правомерно владеющее закрытым ключом, соответствующим открытому ключу, указанному в регистрационном свидетельстве;
  • электронный документ – документ, в котором информация представлена в электронно-цифровой форме и удостоверена посредством электронной цифровой подписи;
  • электронная цифровая подпись – набор электронных цифровых символов, созданный средствами электронной цифровой подписи и подтверждающий достоверность электронного документа, его принадлежность и неизменность содержания;
  • средства электронной цифровой подписи – совокупность программных и технических средств, используемых для создания и проверки подлинности электронной цифровой подписи;
  • открытый ключ электронной цифровой подписи – последовательность электронных цифровых символов, доступная любому лицу и предназначенная для подтверждения подлинности электронной цифровой подписи в электронном документе;
  • закрытый ключ электронной цифровой подписи – последовательность электронных цифровых символов, известная владельцу регистрационного свидетельства и предназначенная для создания электронной цифровой подписи с использованием средств электронной цифровой подписи.

Какие преимущества дает использование ЭЦП на нашем портале?

Ключевые преимущества при использовании ЭЦП через портал электронного правительства:

  • Возможность получения электронных услуг государственных органов в любое удобное для вас время: круглосуточно, семь дней в неделю;
  • Возможность подачи электронных обращений в виртуальные приемные государственных органов области и республики. Ссылка на сервис «Электронных обращений» появляется в правом блоке страниц портала после авторизации пользователя.

Обновление ПО

В начале мая 2022 года команда разработчиков Национального Удостоверяющего Центра анонсировала выход модернизированной версии программного обеспечения НУЦ РК.

Целью перехода на использование нового ПО является облегчение установки корневых сертификатов для юридических и физических лиц. Если раньше пользователям было необходимо прибегать к помощи ПО Tumar CSP, что требовало особых усилий при установке и ограничено ОС Windows, то теперь, с новым ПО все обстоит гораздо проще.

Так в чем же отличие?

  • Корневой сертификат – сертификат, принадлежащий Центру Сертификации, с помощью которого проверяется достоверность других, выданных центром сертификатов. Для того, чтобы программное обеспечение, например, операционная система или браузер смогли корректно проверить сертификат пользователя, необходимо, чтобы корневой сертификат был предварительно установлен в браузере или в операционной системе.
  • Tumar CSP – это программное обеспечение, которое выполняло “внедрение” в операционную систему семейства Windows криптографического алгоритма ГОСТ, который изначально не поддерживается ими. То есть он являлся программным обеспечением для подсистемы криптографии операционной системы, а корневой сертификат – это информация, которой пользуется эта самая подсистема криптографии.

Ранее пользователю приходилось непростым способом устанавливать ПО Tumar CSP. А также, он был привязан к операционной системе Windows, так как полноценно Tumar CSP функционировал только на данной ОС.

Отныне пользователю требуется лишь предустановленный Java, одну из наиболее распространенных ОС (Windows XP/Vista/Seven, Linux) и доступ к сети Интернет.

Более того, весь процесс «Установка ПО – Получение ЭЦП – Получение услуг портала» доступен на Mac OS X. Корневые сертификаты для этой ОС нужно скачивать в чистом виде. Они доступны по этой ссылке.

В этом году планируется реализация соответствующего функционала для ОС Android, далее по плану – поддержка iOS и Windows Mobile.

Сам порядок получения сертификатов изменен не был. Представим его в схематичном виде:

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

Краткие пояснения процедуры получения сертификата

Повторим описание процесса получения сертификатов НУЦ. На портале есть страница «Получения ЭЦП», на которой кратко описаны шаги и даны ссылки на скачивание необходимого ПО, руководства пользователя и бланков документов. Там приведены все необходимые данные, как для физических, так и для юридических лиц.

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

Весь процесс От и До от автора

Для наглядности представления всего процесса, начиная с установки сертификатов, заканчивая получением справки популярной электронной услуги, выложу скрины с пояснениями.

Может быть, с одной стороны это будет выглядеть как повтор инструкций, но всё же, это собственный опыт автора в прохождении этого процесса.

Регистрация на портале

Регистрация на портале реализована обычным, даже, можно сказать, совсем уж упрощенным способом. От меня требуется вписать свой ИИН в нужное поле. После нажатия ссылки «Найти» мои ФИО автоматически внеслись в нужные поля, и потребовалось только обозначить пароль и ввести свой e-mail адрес для получения уведомлений.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

Итак, регистрация прошла успешно и вообще никаких затруднений не возникло.

Получение сертификата

Ссылка «Получить ЭЦП» уже сама намекает, о том, что скрывает за собой всё необходимое. После перехода по ней, откроется страница как в следующем скрине.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

Здесь раскрываем первую вкладку и нажимаем на кнопочки. Сначала скачиваем корневые сертификаты, которые установятся за 2 клика, а потом необходимо скачать Java с его официального сайта. Этот сайт совершенно прост в навигации, как и проста установка самого ПО. На это ушло не более десяти кликов по очевидным кнопкам.

После этих операций пришло время подать заявку в НУЦ на получение сертификата. Для этого нажимаем на кнопку «Подать online-заявку», которая откроет специальную страничку подачи заявления.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

После ввода личных данных необходимо указать адрес ЦОН-а, в который Вам удобно будет подойти для подтверждения Вашей личности о том, что именно вы (а не кто-то другой вместо вас хочет получить ЭЦП).

Местом хранения я выбрал отдельную папку своего компьютера. Далее, вводим код с картинки, нажимаем «Подтвердить» и всё – заявка подана.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

На указанный адрес почты приходит письмо с уведомлением о том, что заявке присвоен номер, с вложенным готовым бланком заявления.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

С этим бланком я тут же пошел в ЦОН и оператор сразу же подтвердил мою заявку и отправил письмо мне на почту. Кстати, не забудьте перед походом в ЦОН захватить свое удостоверение личности!

Итак, придя обратно домой и проверив статус заявки по ссылке с письма, я обнаружил, что уже можно установить свои ключи ЭЦП на компьютер. Сохранить надо в ту же папку, которая была указана при подаче заявки. Не забудьте запомнить пароль по умолчанию на ключ (123456).

Как мне объяснили, при выдаче сертификатов всем выдается одинаковый пароль, который позже можно сменить в личном кабинете (меню Личные настройки ->Обновление пароля на ключи). Одинаковый пароль сделан для удобства пользователя, так как многие забывают свой пароль. Задать его заранее нельзя. Однако, настоятельно рекомендуется после получения сертификатов сменить пароль на более защищенный, через личный кабинет.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

После этого выходит сообщение, что сертификаты установлены и уже можно с радостью «бежать» на портал и получать свои первые справки.
Вспомнив, что мне как раз нужна была адресная справка, я, недолго думая, отправился её получать по этой ссылке.

Не судите строго, что я осознанно выбрал одну из самых легких в получении услуг, но она действительно была мне нужна :).

Получение справки электронной услуги

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

После перехода в интерфейс самой услуги, мой ИИН автоматически ввелся в единственное поле, и мне не оставалось ничего, кроме как нажать «Отправить запрос».

После этого вышла страница с сообщением о том, что сертификат не выбран.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

Следовательно, если я хочу получить услугу, то мне нужно кликнуть на «Выбрать сертификат». После этого действия, открывается меню выбора файла. Находим папку, куда сохраняли ключи, выбираем тот, который начинается на RSA и нажимаем «Открыть».

Почему я выбрал именно сертификат RSA? Что означают наименования других сертификатов, таких как GOST и AUTH_RSA? Поясню:

  • Для старых пользователей, получивших ЭЦП до 30 апреля 2022 года: сертификат GOST – предназначен для подписания запросов, а RSA для аутентификации на портале;
  • Для новых пользователей (физ лица), получивших ЭЦП после 1 мая 2022 года: сертификат RSA – предназначен для подписания запросов, а AUTH_RSA для аутентификации на портале;
  • Для новых пользователей (юр лица): сертификат GOST – предназначен для подписания запросов, а RSA для аутентификации на портале.

По отличиям:

  • GOST – сертификат, созданный по старому криптографическому алгоритму GOST;
  • RSA – сертификат, созданный по новому криптографическому алгоритму RSA.

Запомните эту информацию во избежание каких-либо недоразумений в будущем. Если вы, например, при подписании запроса, выберете сертификат для аутентификации, то запрос не будет подписан.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

Как показано на следующем скрине, появится маленькое окно, запрашивающее пароль. Тут необходимо ввести пароль по умолчанию (123456), который упоминался при установке ключей.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

И вот, вышло то самое, важное окошко с волшебной кнопкой «Подписать».

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

После подписания, вышла страница результатов моего запроса, которая при обновлении статуса сразу же выдала мне мою долгожданную справку.

Если вы уже один раз выбрали нужный сертификат и ввели пароль, то при получении следующих услуг система его запрашивать не будет и данные вашего сертификата автоматически введутся в систему.

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий / Хабр

Вот и всё! Если действовать по инструкции – то никаких затруднений в использовании электронных услуг не будет. Все доступно и понятно.

И вообще, наличие сертификата и доступа к порталу – это действительно удобно.

Имея у себя на компьютере установленный сертификат, на получение этой справки у меня ушло всего 2 минуты. А представьте себе, сколько времени я потратил бы, чтобы сходить за ней в ЦОН. Явно больше чем 2 минуты!

Надеемся, что эта статья поможет вам с некоторыми вопросами. Мы всегда открыты для пожеланий и вопросов, так как стремимся к лучшему. Если возникнут вопросы, и мы обязательно ответим на них в следующих статьях и через формы обратной связи!

Важно! Для того, чтобы проверить подлинность электронных справок, полученных с портала, используйте сервис проверки документов. А также, через сервис проверки чеков можно удостовериться в том, что онлайн-оплата проведена через портал, терминал БТА банка или «Кабинет налогоплательщика».

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *