В статье описана типичная схема подобной интеграции, основанная на следующих сценариях использования Плагина:
Данные сценарии предполагают клиент-серверное взаимодействие, написание клиентских скриптов на JavaScript и соответствующих им серверных вызовов OpenSSL.
Если вы открываете локальную страницу, убедитесь, что в настройках расширения Адаптер Рутокен Плагин включена опция "Разрешить открывать локальные файлы по ссылкам" |
Так как все функции Плагина выполняются в отдельных потоках, не заставляя браузер ждать выполнения операций, его интерфейс постороен на промисах. Промисы - это способ организации асинхронного кода. Универсальный метод добавления обработчиков:
promise.then(onFulfilled, onRejected) // onFulfilled – функция, которая будет вызвана с результатом при успешном выполнении асинхронной функции. // onRejected – функция, которая будет вызвана с ошибкой при ошибке выполнения асинхроннной функции. |
Любой клиентский сценарий начинается с поиска подключенных к компьютеру USB-устройств Рутокен.
function handleError(reason) { if (isNaN(reason.message)) { alert(reason); } else { var errorCodes = plugin.errorCodes; switch (parseInt(reason.message)) { case errorCodes.PIN_INCORRECT: alert("Неверный PIN"); break; default: alert("Неизвестная ошибка #" + reason.message); } } } // ... plugin.enumerateDevices().then(function(devices) { if (devices.length > 0) { // ... } else { return Promise.reject("Рутокен не обнаружен"); } }, handleError) |
При этом вызове возвращается список идентификаторов подключенных устройств. Идентификатор представляет собой число, связанное с номером слота, к которому подключено устройство. При повторном перечислении это число может отличаться для одного и того же устройства.
Стоит заметить, что вызов этот произойдет асинхронно. Первый аргумент метода then, вызывается в случае успешного выполнения Primve enumerateDevices, второй --в случае ошибки. В данном примере был написан обобщенный обработчик ошибок. Его можно расширить и использовать в дальнейшем в своих проектах.
Рутокен Плагин определяет все подключенные к компьютеру USВ-устройства Рутокен ЭЦП. Поэтому следующим шагом следует определить тип устройства.
Для определения типа устройства следует использовать функцию getRutokenModelName() репозитория https://github.com/AktivCo/rutoken-model-by-hw-features-js
Параметр функции getDeviceInfo TOKEN_INFO_DEVICE_TYPE – устарел и не поддерживается.
С помощью функции getDeviceInfo можно получить:
// вызывается метод Promise, который должен вернуть Id устройства .then(function(rutokenHandle){ options={} return plugin.changePin(rutokenHandle, "12345678", "12345678", options) }) .then(function() { alert("PIN изменен успешно"); }, handleError) |
Здесь первым параметром выступает старый PIN-код, а вторым новый PIN-код.
1. На токене могут храниться 4 категории сертификатов:
2. Для чтения сертификатов, хранящихся на устройстве, не требуется авторизация на устройство.
// вызывается метод Promise, который должен вернуть Id устройства .then(function(rutokenHandle) { return plugin.enumerateCertificates(rutokenHandle, plugin.CERT_CATEGORY_USER); }) // Вывод первого найденного сертификата на Рутокене .then(function(certs) { if (certs.length > 0) { // ... } else { return Promise.reject("Сертификат на Рутокен не обнаружен"); } }, handleError) |
3. Сертификат можно экспортировать в PEM-формате:
// вызывается метод Promise, который должен вернуть Id устройства и Id сертификата .then(function(rutokenHandle, certHandle) { return plugin.getCertificate(rutokenHandle, certHandle); }) // Отображение подписанных данных в формате PEM .then(function(pem) { alert(pem); }, handleError) |
Получится примерно такая строка:
-----BEGIN CERTIFICATE----- MIIBmjCCAUegAwIBAgIBATAKBgYqhQMCAgMFADBUMQswCQYDVQQGEwJSVTEPMA0G A1UEBxMGTW9zY293MSIwIAYDVQQKFBlPT08gIkdhcmFudC1QYXJrLVRlbGVjb20i MRAwDgYDVQQDEwdUZXN0IENBMB4XDTE0MTIyMjE2NTEyNVoXDTE1MTIyMjE2NTEy NVowEDEOMAwGA1UEAxMFZmZmZmYwYzAcBgYqhQMCAhMwEgYHKoUDAgIjAQYHKoUD AgIeAQNDAARADKA/O1Zw50PzMpcNkWnW39mAJcTehAhkQ2Vg7bHkIwIdf7zPe2Px HyAr6lH+stqdACK6sFYmkZ58cBjzL0WBwaNEMEIwJQYDVR0lBB4wHAYIKwYBBQUH AwIGCCsGAQUFBwMEBgYpAQEBAQIwCwYDVR0PBAQDAgKkMAwGA1UdEwEB/wQCMAAw CgYGKoUDAgIDBQADQQD5TY55KbwADGKJRK+bwCGZw24sdIyayIX5dn9hrKkNrZsW detWY3KJFylSulykS/dfJ871IT+8dXPU5A7WqG4+ -----END CERTIFICATE----- |
4. Сертификат можно распарсить вызовом функции parseCertificate и получить из него DN Subject, DN Issuer, расширения, значение открытого ключа, подпись, серийный номер, срок действия и т.п.
5. Сертификат можно записать на устройство.
var certpem = "-----BEGIN CERTIFICATE----- MIIBmjCCAUegAwIBAgIBATAKBgYqhQMCAgMFADBUMQswCQYDVQQGEwJSVTEPMA0G A1UEBxMGTW9zY293MSIwIAYDVQQKFBlPT08gIkdhcmFudC1QYXJrLVRlbGVjb20i MRAwDgYDVQQDEwdUZXN0IENBMB4XDTE0MTIyMjE2NTEyNVoXDTE1MTIyMjE2NTEy NVowEDEOMAwGA1UEAxMFZmZmZmYwYzAcBgYqhQMCAhMwEgYHKoUDAgIjAQYHKoUD AgIeAQNDAARADKA/O1Zw50PzMpcNkWnW39mAJcTehAhkQ2Vg7bHkIwIdf7zPe2Px HyAr6lH+stqdACK6sFYmkZ58cBjzL0WBwaNEMEIwJQYDVR0lBB4wHAYIKwYBBQUH AwIGCCsGAQUFBwMEBgYpAQEBAQIwCwYDVR0PBAQDAgKkMAwGA1UdEwEB/wQCMAAw CgYGKoUDAgIDBQADQQD5TY55KbwADGKJRK+bwCGZw24sdIyayIX5dn9hrKkNrZsW detWY3KJFylSulykS/dfJ871IT+8dXPU5A7WqG4+ -----END CERTIFICATE-----"; // ... // вызывается метод Promise, который должен вернуть Id устройства и Id сертификата .then(function(rutokenHandle, certPem) { return plugin.importCertificate(rutokenHandle, certPem, plugin.CERT_CATEGORY_USER); }) // Отображение подписанных данных в формате PEM .then(function() { alert("Сертификат импортирован успешно"); }, handleError) |
6. Вызовом функции deleteCertificate можно удалить сертификат с токена.
1. Для получения дескрипторов ключевых пар, хранящихся на устройстве, требуется ввод PIN-кода. Следует понимать, что само значение закрытого ключ получено быть не может, так как ключ является неизвлекаемым.
// вызывается метод Promise, который должен вернуть Id устройства .then(function(rutokenHandle) { return plugin.enumerateKeys(rutokenHandle, ""); }) .then(function(keys) { if (keys.length > 0) { // ... } else { return Promise.reject("Ключи на Рутокене не обнаружены"); } }, handleError) |
2. Для генерации ключевой пары требуется ввод PIN-кода. При генерации ключа можно задать параметры ключевой пары. Если опция не задана, будут выбраны параметры А для любого из алгоритмов ГОСТ.
// вызывается метод Promise, который должен вернуть Id устройства .then(function(rutokenHandle) { return plugin.generateKeyPair(rutokenHandle, undefined, marker, options); }) .then(function(keyId) { alert("Ключевая пара сгенерирована успешно"); }, handleError) |
3. С помощью функции deleteKeyPair ключевая пара может быть удалена с токена.
1. Для создание подписи используется метод sign:
// Подпись .then(function(certHandle, textToSign) { var options = {}; return plugin.sign(rutokenHandle, certHandle, textToSign, plugin.DATA_FORMAT_PLAIN, options); ) |
Последние аргумент – опции функции. Их подробный список можно найти в документации, некоторые из них:
Предпоследний аргумент определяет формат подписываемых данных:
2. Для проверки подписи используется метод verify:
// Проверка подписи .then(function(certPem, CA, cms) { var options = {certificates: [certPem], CA: CA}; return plugin.verify(rutokenHandle, cms, options); }) |
Последние аргумент – опции функции. Их подробный список можно найти в документации, некоторые из них:
См. инструкцию Интеграция ГОСТ 2012 с Рутокен ЭЦП и OpenSSL 1.1.0 или новее, раздел Установка и настройка OpenSSL для работы с rtengine 0.7.x
Теперь перейдем к реализации законченных пользовательских сценариев.
Последовательность вызовов в клиентском скрипте будет следующей:
Далее запрос отправляется на сервер, где на его основе выдается сертификат.
Для этого на сервере должен быть установлен и правильно сконфигурирован openssl версии от 1.0 и развернут функционал УЦ.
1. Генерация улюча УЦ:
openssl genpkey -algorithm gost2012_256 -pkeyopt paramset:A -out ca.key |
После этого в файле ca.key будет создан закрытый ключ
2. Создание самоподписанного сертификата УЦ:
openssl req -utf8 -x509 -key ca.key -out ca.crt |
После ввода необходимой информации об издателе в файле ca.crt будет создан сертификат УЦ.
Полученный от клиента запрос сохраняем в файл user.csr и выдаем на его основе сертификат (без модификации данных из запроса):
openssl ca -keyfile ca.key -cert ca.crt -in user.csr -out user.crt -batch |
После этого в файле user.crt создается сертификат пользователя в PEM формате. Его следует отправить на клиент.
Дальнейшая последовательность вызовов на клиенте:
Ключевая пара при этом должна быть создана в формате, совместимом с библиотекой rtPKCS11ECP для Рутокен ЭЦП .
Последовательность вызовов на клиенте:
Подпись получается в base64-формате. При проверке ее на сервере с помощью openssl подпись следует обрамить заголовками, чтобы сделать из нее PEM. Выглядеть подобная подпись будет примерно так:
-----BEGIN CMS----- MIIDUQYJKoZIhvcNAQcCoIIDQjCCAz4CAQExDDAKBgYqhQMCAgkFADCBygYJKoZI hvcNAQcBoIG8BIG5PCFQSU5QQURGSUxFIFVURjg+PFY+0JLRi9C/0L7Qu9C90LjR gtGMINCw0YPRgtC10L3RgtC40YTQuNC60LDRhtC40Y4/PCE+c2VydmVyLXJhbmRv bS1kYXRhZTI6ZGE6MmM6MDU6MGI6MzY6MjU6MzQ6YzM6NDk6Nzk6Mzk6YmI6MmY6 YzU6Mzc6ZGI6MzA6MTQ6NDQ6ODM6NjY6Njk6NmI6OWY6YTU6MDk6MzQ6YmY6YzQ6 NzY6YzmgggGeMIIBmjCCAUegAwIBAgIBATAKBgYqhQMCAgMFADBUMQswCQYDVQQG EwJSVTEPMA0GA1UEBxMGTW9zY293MSIwIAYDVQQKFBlPT08gIkdhcmFudC1QYXJr LVRlbGVjb20iMRAwDgYDVQQDEwdUZXN0IENBMB4XDTE0MTIyMjE2NTEyNVoXDTE1 MTIyMjE2NTEyNVowEDEOMAwGA1UEAxMFZmZmZmYwYzAcBgYqhQMCAhMwEgYHKoUD AgIjAQYHKoUDAgIeAQNDAARADKA/O1Zw50PzMpcNkWnW39mAJcTehAhkQ2Vg7bHk IwIdf7zPe2PxHyAr6lH+stqdACK6sFYmkZ58cBjzL0WBwaNEMEIwJQYDVR0lBB4w HAYIKwYBBQUHAwIGCCsGAQUFBwMEBgYpAQEBAQIwCwYDVR0PBAQDAgKkMAwGA1Ud EwEB/wQCMAAwCgYGKoUDAgIDBQADQQD5TY55KbwADGKJRK+bwCGZw24sdIyayIX5 dn9hrKkNrZsWdetWY3KJFylSulykS/dfJ871IT+8dXPU5A7WqG4+MYG7MIG4AgEB MFkwVDELMAkGA1UEBhMCUlUxDzANBgNVBAcTBk1vc2NvdzEiMCAGA1UEChQZT09P ICJHYXJhbnQtUGFyay1UZWxlY29tIjEQMA4GA1UEAxMHVGVzdCBDQQIBATAKBgYq hQMCAgkFADAKBgYqhQMCAhMFAARAco5PumEfUYVcLMb1cnzETNOuWC8Goda8pdUL W5ASK+tztCwM7wpXgAy+Y6/sLtClO9sh8dKnAaEY2Yavg3altQ== -----END CMS----- |
openssl cms -verify -in sign.cms -inform PEM -CAfile ca.crt -out data.file -certsout user.cr |
Здесь sign.cms — файл, в котором находится подпись, ca.crt — файл с корневыми сертификатами, до одного из которых должна выстроиться цепочка, data.file — файл, в который будет сохранены подписанные данные, user.crt — файл, в который будет сохранен пользовательский сертификат. Именно из data.file нужно извлечь данные отсоединить последние 32 символа и сравнить salt.
Если на сервере нужно получить информацию из сертификата, то парсить его можно так:
openssl x509 -in cert.pem -noout -text |
openssl x509 -in cert.pem -noout -serial |
openssl x509 -in cert.pem -noout -subject |
openssl x509 -in cert.pem -noout -issuer |
openssl x509 -in cert.pem -noout -email |
openssl x509 -in cert.pem -noout -startdate |
openssl x509 -in cert.pem -noout -enddate |
Общая схема аутентификации, используемая в Рутокен Плагин, выглядит следующим образом:
Реализация данной схемы ничем принципиально не отличается от «Регистрация, сертификат уже имеется, выдан внешним УЦ».
Проверка подписи на сервере описана выше.
Для того, чтобы обеспечить конфиденциальность обмена данными между клиентом и сервером в плагине предусмотрено шифрование/расшифрование данных. Данные шифруются в формате CMS. Для того, чтобы зашифровать данные в формате CMS, требуется сертификат открытого ключа «адресата». При этом расшифровать такое сообщение сможет только владелец закрытого ключа. При шифровании данных для сервера рекомендуется хранить сертификат сервера на Рутокен ЭЦП . Этот сертификат может быть записан на устройство при регистрации пользователя на портале. Для этого следует использовать функцию importCertificate, при этом в качестве параметра category следует передать CERT_CATEGORY_OTHER. Для использования в функции cmsEncrypt нужно получить тело сертификата по его дескриптору с помощью функции getCertificate. При этом дескриптор является уникальным и неизменным и может быть сохранен в учетной записи пользователя на сервере при импорте сертификата сервера. Для того, чтобы использовалось аппаратное шифрование по ГОСТ 28147-89, требуется установить опцию useHardwareEncryption в true. В противном случае будет использована быстрая программная реализация ГОСТ 28147-89.
Последовательность вызовов приведена на картинке:
var data = "some data" var recipientsCertsInPem = [ String.raw`-----BEGIN CERTIFICATE----- MIICEjCCAb+gAwIBAgIJAL+E0An4CJgVMAoGCCqFAwcBAQMCMHkxCzAJBgNVBAYT AlJVMQ8wDQYDVQQIDAZSdXNzaWExDzANBgNVBAcMBk1vc2NvdzEXMBUGA1UECgwO WkFPIEFrdGl2LVNvZnQxEDAOBgNVBAsMB1J1dG9rZW4xHTAbBgNVBAMMFFJ1dG9r ZW4gVEVTVCBDQSBHT1NUMB4XDTIwMTAxOTE3MjIyN1oXDTIxMTAxOTE3MjIyN1ow MjEMMAoGA1UEAwwDbWVtMQswCQYDVQQGEwJSVTEVMBMGA1UECAwM0JzQvtGB0LrQ stCwMGYwHwYIKoUDBwEBAQEwEwYHKoUDAgIjAQYIKoUDBwEBAgIDQwAEQIyKtSP4 WYjewCNr52fNMYDVwFPIwWgWdOi+DpJ47pBn4g6rILdZfvkAfemMs7a74is+mPyg 401mjbZPhDXCRt2jajBoMAsGA1UdDwQEAwIGwDATBgNVHSUEDDAKBggrBgEFBQcD BDATBgNVHSAEDDAKMAgGBiqFA2RxATAvBgUqhQNkbwQmDCTQodCa0JfQmCAi0KDR g9GC0L7QutC10L0g0K3QptCfIDIuMCIwCgYIKoUDBwEBAwIDQQCJvbdPKR9QO4zP Xs4SK/dhzNdNzYmyCOoi7qUBYc4laKwUnhV88ENR5+VDee05w+JMIIJFuelg/QN3 W/W6SC2v -----END CERTIFICATE-----` ] // вызывается метод Promise, который должен вернуть Id устройства .then(function (rutokenHandle) { var options = {}; return plugin.cmsEncrypt(rutokenHandle, "", recipientsCertsInPem, data, options); }) .then(function(msg) { alert(msg); }, handleError) |
Перед расшифрованием сообщение нужно обрамить PEM-заголовками "-----BEGIN PKCS7-----" и "-----END PKCS7-----":
openssl cms -decrypt -binary -in message.cms -inform PEM -recip respondent.cer -inkey recipient.key -out drecipient.crt |
recipient.crt — сертификат того, для кого зашифровано сообщение, recipient.key — ключ того, для кого зашифровано сообщение.
Для расшифрования данных, полученных с сервера, предназначена функция cmsDecrypt. Так как сервер шифрует для клиента, используя его сертификат, то в качестве keyId должен быть передан дескриптор закрытого ключа клиента, соответствующий открытому ключу в сертификате. Этот дескриптор является уникальным и неизменным и потому может быть сохранен в учетной записи пользователя на сервере. Кроме того, дескриптор ключа пользователя может быть получен явным образом, путем вызова функции getKeyByCertificate.
openssl cms -encrypt -binary -gost28147-paramset_a-cfb -in data.file -out message.enc -outform PEM user.crt |
// вызывается метод Promise. Переменные rutokenHandle и keyHandle должны быть во внешном блоке. .then(function () { var options = {}; return plugin.cmsDecrypt(rutokenHandle, keyHandle, cms, options); }) .then(function(msg) { alert(msg); }, handleError) |
Данные ссылки могут быть полезны разработчикам инфосистем с поддержкой ЭП на базе Рутокен Плагин и OpenSSL:
Пример интеграции Рутокен Плагин в ДБО
Веб-сервис генерации ключей, формирования запросов, управления сертификатами
Документация по использованию утилиты OpenSSL с российскими криптоалгоритмами