Обзор rutoken – персонального средства аутентификации и хранения сертификатов
Дома и на работе при использовании почтовых программ и доступе к корпоративным ресурсам нам приходится пользоваться паролями. Чаще всего пароли для разных программ и ресурсов различаются, и необходимо запоминать множество непростых символьных сочетаний либо записывать их на бумаге, что явно не рекомендуется службами безопасности вашей компании. В меньшей степени это относится к использованию компьютера дома, но и здесь бывает необходимо защитить определенные ресурсы и программы от доступа.
Вместо использования доступа по паролю к корпоративным ресурсам, для входа в домен и при использовании корпоративной почты можно использовать аппаратную аутентификацию и защиту электронной переписки в сетях, построенных на базе Windows 2000/ХР/2003. В предлагаемом решении используются встроенные инструменты безопасности Windows и электронные идентификаторы Rutoken в качестве носителей ключевой информации.
Что обеспечивается при использовании этого продукта? Авторизация пользователей (вход в домен при подключении токена и блокирование сессии после его отсоединения). При работе с почтой — электронная цифровая подпись почтовых сообщений и их шифрование. Доступ к корпоративным ресурсам по предъявлении токена и, конечно, надежное хранение и использование сертификатов. Часть из перечисленных задач может быть использована и на домашнем компьютере, например, при работе с почтой, а также для удаленного подключения к корпоративным ресурсам. Давайте разберем, что и как необходимо выполнить для реализации перечисленных задач.
Токен как дополнительное устройство не может быть опознан операционной системой, поскольку он не является стандартным оборудованием. Невозможна и установка токена посредством inf-файла, поскольку для корректной установки требуется измерение некоторых параметров для автоматической конфигурации драйвера. Поэтому приходится для установки использовать специальное программное обеспечение. И вот что еще следует помнить. Не подключайте токен к компьютеру до того, как вы установите драйвер. Если все же подключение будет выполнено ранее, остановите работу стандартного мастера установки USB-устройств, извлеките ключ из порта и установите драйвер. Помимо драйверов в процессе инсталляции на диск будут скопированы и утилиты для работы с токеном (утилита администрирования и браузер сертификатов). Количество одновременно используемых токенов зависит от операционной системы и количества USB-портов.
Электронный идентификатор Rutoken — персональное средство аутентификации и хранения данных (сертификатов в данном случае). В нем имеется аппаратная реализация российского стандарта шифрования. Этот токен также совместим и с сертифицированными российскими криптопровайдерами от компаний “Крипто-Про”, “Анкад”, “Сигнал-Ком”.
Прежде чем приступить к настройке корпоративной сети и ее ресурсов для работы с токенами и цифровыми сертификатами, предлагаю провести небольшое тестирование этого продукта, для чего получить электронный сертификат и подключить его для работы с почтой, например с почтовым клиентом The Bat!.
Опять же, чтобы не развертывать в своей сети службы сертификации (их можно развернуть только на серверных платформах), можно воспользоваться услугами тестовых удостоверяющих центров, например тестовым центром сертификации “Крипто-Про” либо тестовым центром сертификации e-Notary компании “Сигнал-Ком”. Если вы выбрали последний, вам будет предложено вначале познакомиться с “Соглашением на изготовление тестовых сертификатов удостоверяющим центром e-Notary”. В случае согласия будет предложено выбрать вариант подачи запроса на изготовление сертификата: либо путем передачи файла с сформированным предварительно запросом, либо путем генерации ключей и формирования запроса встроенными средствами браузера. Для простоты выберем второй вариант.
Подключите к USB-порту ваш токен. Обратите внимание, что все токены изначально имеют одинаковый PIN-код пользователя — 12345678. Пока вы занимаетесь тестированием, этот код можно не менять, но при организации нормальной работы лучше, если вы его замените, чтобы никто иной не мог получить доступ к вашему закрытому ключу. (Именно закрытый ключ используется для формирования подписи и расшифровывания данных.) Для изменения кода воспользуйтесь программой администрирования. С ее же помощью вашему токену можно присвоить и уникальное имя.
Выбрав вариант формирования запроса через браузер, вы попадете на страницу, где вам потребуется заполнить о себе некоторые данные (для тестовых целей они не критичны, и обязательными являются лишь два поля — имя и почтовый адрес). Далее — выбор параметров ключевой пары. Здесь вы должны выбрать из выпадающего списка криптопровайдер Aktiv Rutoken CSP v1.0 (можно взять и любой иной, но мы же тестируем токен). Затем выбрать вариант использования ключей — для обмена данными, для их подписи или для обоих вариантов (выбирайте последний). Далее — выбор размера ключа. Выбранный на предыдущем шаге криптопровайдер позволяет формировать ключи длиной от 512 до 16 384 байт. (Чем больше значение длины ключа, тем меньше вероятность взлома зашифрованных данных.)
Из остальных опций рекомендую выбрать “Пометить ключ как экспортируемый”. В этом случае после его установки вы сможете создать резервную копию пары ключей и сертификата и сохранить их в виде файла. Зачем? На случай утраты токена или его повреждения. Остальные опции можно не изменять и далее запустить процесс формирования запроса. После того как запрос будет обработан, вы сможете установить сертификат на компьютер. Обратите внимание на следующий момент. Давайте откроем браузер сертификатов (мы уже говорили, что он устанавливается вместе с драйверами). После того как вы сформируете запрос, на токене появится новый контейнер с парой ключей. В параметрах ключа будет указана его длина, область применения и значение открытого (публичного) ключа. После установки сертификата информация изменится. В контейнере будет уже не только ключ, но и сертификат со всем своими характеристиками: имя владельца, его почтовый адрес, иная информация, указанная вами при формировании запроса. Кроме того что сертификат будет помещен на токен, он также будет установлен в личное хранилище сертификатов, где его можно будет просмотреть через свойства браузера.
Теперь у нас есть ключи и сертификат, с помощью которых можно подписывать и шифровать корреспонденцию. Посмотрим, как это можно сделать в почтовом клиенте The Bat! (про использование подписи и шифрования в MS Outlook подробно рассказано в документации на рассматриваемый продукт).
Откройте свойства вашего почтового ящика и на вкладке “Общие сведения” кликните по кнопке “Сертификаты”. Откроется стандартное окно для просмотра сертификатов. Убедитесь, что полученный вами сертификат в тестовом УЦ доступен. Перейдите на вкладку “Параметры”. Здесь можно отключить опцию “Авто — OpenPGP”, оставив лишь “Авто — S/Mime”. Кроме того, отметьте опции, которые вы хотите использовать: “Подписать перед отправкой” и “Зашифровать перед отправкой”. Вторую опцию лучше не выбирать до тех пор, пока вы не обменяетесь открытыми ключами с теми, с кем будете вести переписку с шифрованием.
Итак, если вы будете подписывать свое письмо, то после того, как вы нажмете кнопку “Сохранить” или “Отправить”, будет вызван диалог выбора сертификата, на котором вы будете выполнять подпись. Обратите внимание, что сертификат привязан к почтовому адресу. Поэтому, если почтовый адрес, с которого вы пишете и отправляете письмо, отличается от того, что включен в сертификат, почтовый клиент не сможет подписать письмо. Если адреса совпадают, письмо будет подписано и отправится вашему респонденту вместе с сертификатом и открытым ключом. С их помощью ваш респондент сможет проверить подлинность подписи (не изменилось ли письмо во время доставки) и убедиться, что именно вы подписали это письмо. Для того чтобы использовать этот сертификат и ключ для шифрования писем в ваш адрес, ваш респондент должен будет импортировать полученный сертификат в хранилище. Аналогичным образом он пришлет вам свой сертификат, после чего вы сможете не только подписывать письма, но и шифровать их в адрес друг друга.
На этом проверку работы с токеном можно считать завершенной и переходить к настройке сети вашей организации и корпоративных приложений. Первое, с чего следует начать, — установка службы сертификации Microsoft. С ее помощью вы будете выписывать сертификаты сотрудникам, сохранять их на токенах и выдавать в работу. Для удобства можно будет создать шаблоны сертификатов и использовать их.
Для использования цифровой подписи и шифрования почтовых сообщений необходимо будет выполнить настройку почтовых клиентов. Для обеспечения доступа пользователей в домен по своим сертификатам потребуется выдавать сертификаты типа “Пользователь со смарт-картой” или “Вход со смарт-картой”. Далее необходимо настроить учетные записи пользователей домена, установив в параметрах учетной записи флаг “Для интерактивного входа в сеть нужна смарт-карта”.
Дополнительные настройки требуются и для создания защищенного подключения к веб-ресурсам организации (в документации приводится пример настройки веб-ресурса под управлением IIS), можно настроить доступ по сертификатам к терминальному серверу, к сетям VPN. Степень защиты доступа в таких случаях существенно возрастает.
В основном все настройки выполняются администратором сети, настройка рабочих мест требует существенно меньших усилий. Поэтому внедрение системы аппаратной аутентификации, цифровой подписи и шифрования не будет представлять особых сложностей для сотрудников. К тому же невелика и величина расходов по созданию такой системы. Для начала работы вполне достаточно одного стартового комплекта, включающего руководство по внедрению, утилиты для работы с токенами, драйвера и собственно один электронный идентификатор Rutoken. Стартового комплекта достаточно, для того чтобы произвести все необходимые настройки в сети и подготовиться к переходу от обычной парольной защиты к надежной двухфакторной аутентификации на основе Rutoken. Для такой миграции потребуется лишь приобрести необходимое количество идентификаторов Rutoken.
Опыт применения технологии рутокен для регистрации и авторизации пользователей в системе (часть 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()
}
})
Чтобы успешно использовать Рутокен плагин и вообще понимать, как работает этот код, нужно разобраться, что такое Promise. Есть
отличная статья
на эту тему, в которой имеется все что нужно для понимания Promise.
Напомню, что для нашей задачи необходимо сначала создать ключевую пару на Рутокене, а затем сформировать запрос на сертификат, который после этого будет отправлен на сервер для получения сертификата.
Для написания своих клиентский приложения я использую фреймворк Ext Js, но это абсолютно не важно. Уверен, что понять весь этот код можно и без знания Ext Js.
Перейдем к реализации. Для начала нужно получить соединение с Рутокеном. Для этого был создан класс, который будет за это отвечать:
Для работы с этим классом просто нужно создать его объект, а потом вызвать функцию connectToDevice. В результате эта функция возвращает список, состоящий из плагина и номера первого Рутокена. Функция connectToDevice принимает параметр rutoken. Этот объект получается копированием части содержимого кода, приведенного в начале статьи:
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 класса ecpexpert.rutoken.ConnectToDevice, код которого был приведен ранее. Это действие я делаю в следующем классе, который отвечает за регистрацию пользователя:
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 получает объекты классов ecpexpert.rutoken.RutokenInit (rutoken), ecpexpert.rutoken.ConnectToDevice (device), третьим параметром передается scope на ViewController, чтобы у меня была возможность работать с данными с формы (рассмотрение данной части выходит за рамки статьи).
Считаю, что код имеет достаточное количество комментариев, чтобы можно было понять, как он работает. Обращу внимание только на некоторые моменты.
Во-первых, обратите внимание на часть кода помеченную комментарием «Формируем запрос createPkcs10 на выдачу сертификата». Рассмотрим переменную subject. Она представляет собой ассоциативный массив, который содержит в себе данные пользователя. Эти данные потом будут отражаться в самом сертификате. Дело в том, что по умолчанию можно менять только значения этого массива, но нельзя добавлять новые или удалять старые элементы этого массива. Обратите на это внимание, иначе ваша функция для создания запроса работать не будет.
Во-вторых, хочу обратить внимание на то, что алгоритм, с помощью которого формируется ключевая пара (в моем случае PUBLIC_KEY_ALGORITHM_GOST3410_2022_256), должен соответствовать алгоритму хеширования в переменной options (в моем случае это «hashAlgorithm»: plugin.HASH_TYPE_GOST3411_12_256). Если будете использовать другой алгоритм создания ключевой пары, то алгоритм хеширования должен быть соответствующий. В противном случае ваша функция для создания запроса также работать не будет.
После формирования запроса на создание сертификата, происходит отправка этого запроса на сервер, и в ответ приходит сам сертификат, который успешно импортируется на Рутокен.
Отлично! Мы произвели регистрацию пользователя в системе. Теперь мы имеем на своем Рутокене сертификат с неэкспортируемой ключевой парой, а также удостоверяющий центр знает о сертификате, который записан на Рутокене, так как он сам его выдал с помощью первой команды, описанной в данной статье.
Перейдем к авторизации пользователя. Алгоритм процесса авторизации описан тут.
Для программного описания процесса авторизации мной был создан класс ecpexpert.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 с логическим значением, которое говорит о том, что прошла ли наша авторизация успешно или нет.
В заключение приведу полезную ссылку, которая поможет вам протестировать работоспособность функций Рутокен плагина.
Надеюсь, этот цикл статей был полезен для вас, желаю удачи и спасибо за внимание!