Вбудовування електронного підпису системи з WEB-інтерфейсом за допомогою браузерного плагіна і openssl



Кілька років тому нашою компанією був зроблений продукт Рутокен Плагін, який призначений для вбудовування ЕЦП системи з web-інтерфейсом. Базуючись на отриманому досвіді інтеграції продукту в реальні проекти мені хочеться відзначити, що нерідко розробники для реалізації серверної частини воліють використовувати підтримує російські криптоалгоритми openssl.

У даній статті буде розписана типова схема подібної інтеграції, заснована на наступних сценаріях використання плагіна:

  • Реєстрація на порталі (з видачею сертифіката або по наявному сертифікату)
  • Сувора аутентифікація на порталі
  • Електронний підпис даних та/або файлів у форматі CMS
  • Шифрування даних і/або файлів у форматі CMS


Дані сценарії припускають клієнт-серверне взаємодія, написання клієнтських скриптів на JavaScript і відповідних їм серверних викликів openssl.

Подробиці під катом.


Загальні операції
Операції з пристроєм

Пошук підключених пристроїв

Будь клієнтський сценарій починається з пошуку підключених до комп'ютера USB-пристроїв Рутокен. У контексті даної статті акцент робиться на пристрій Рутокен ЕЦП.
var devices = Array();

try 
{
devices = plugin.enumerateDevices();
}
catch (error) 
{
console.log(error);
}


При цьому повертається список ідентифікаторів підключених пристроїв. Ідентифікатор представляє собою число, пов'язане з номером слота, до якого підключено пристрій. При повторному перерахування це число може відрізнятися для одного і того ж пристрою.

Рутокен Плагін визначає всі підключені до комп'ютера USB-пристрої Рутокен ЕЦП, Рутокен PINPad, Рутокен WEB. Тому наступним кроком слід визначити тип пристрою.

Отримання інформації про пристрій

Для визначення типу пристрою слід використовувати функцію getDeviceInfo з параметром TOKEN_INFO_DEVICE_TYPE. Значення цієї константи міститься в об'єкті плагіна.
var type;

try
{ 
type = plugin.getDeviceInfo(deviceId, plugin.TOKEN_INFO_DEVICE_TYPE);
}
catch (error) 
{
console.log(error);
}

switch (type) 
{
case plugin.TOKEN_TYPE_UNKNOWN:
message = "Невідомий пристрій";
break;
case plugin.TOKEN_TYPE_RUTOKEN_ECP:
message = "Рутокен ЕЦП";
break;
case plugin.TOKEN_TYPE_RUTOKEN_WEB:
message = "Рутокен Web";
break;
case plugin.TOKEN_TYPE_RUTOKEN_PINPAD_2:
message = "Рутокен PINPad";
break;
}


Також за допомогою функції getDeviceInfo можна отримати:
  • модель пристрою
  • мітку пристрою
  • серійний номер пристрою
  • інформацію про те, залягання користувач на пристрій


Зміна PIN-коду

Приклад зміни PIN-коду на пристрої:
var options = {};

try
{ 
certs = plugin.changePin("12345678", "12345671", options);
}
catch (error) 
{
console.log(error);
}


Тут першим параметром виступає старий PIN-код, а другим новий PIN-код.

Робота з сертифікатами
1. На токені може зберігатися 3 категорії сертифікатів:

  • користувальницькі константа в плагіні CERT_CATEGORY_USER)
    Це сертифікати, пов'язані з закритим ключем користувача. Застосовуються, наприклад, для підпису CMS/PKCS#7, аутентифікації користувача в протоколі TLS.
    Якщо сертифікат імпортується як користувальницький, то при імпорті буде проведений пошук в пристрої відповідного йому закритого ключа. Якщо такий ключ знайдено, то сертифікат буде «прив'язаний» до цього ключа. Якщо ключ знайдений не буде, то повернеться помилка.
  • кореневі (константа в плагіні CERT_CATEGORY_CA)
    Це сертифікати видавців сертифікатів, які використовуються для перевірки підпису під сертифікатами. Подібна перевірка підпису (побудова ланцюжка довіри) дозволяє визначити, чи довіряє користувач підпису іншого користувача. Наприклад, у функції плагіна verify є режим перевірки сертифіката, на якому було підписано повідомлення. При цьому використовується сховище кореневих сертифікатів на токені, створене імпортом кореневих сертифікатів на токен.
  • інші (константа в плагіні CERT_CATEGORY_OTHER)
    Це сертифікати, які не пов'язані із закритим ключем і не є кореневими.


2. Для читання сертифікатів, що зберігаються на пристрої, не потрібна авторизація на пристрій.

Приклад читання користувальницьких сертифікатів з пристрою:
var certs = Array();

try
{ 
certs = plugin.enumerateCertificates(deviceId, plugin.CERT_CATEGORY_USER);
}
catch (error) 
{
console.log(error);
}


3. Сертифікат можна експортувати в PEM-форматі:
var certpem;

try
{ 
certpem = plugin.getCertificate(deviceId, certId);
}
catch (error) 
{
console.log(error);
}


Вийде приблизно такий рядок:
----- 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-----";

try
{ 
plugin.importCertificate(deviceId, certpem, plugin.CERT_CATEGORY_USER);
}
catch (error) 
{
console.log(error);
}


6. Викликом функції deleteCertificate можна видалити сертифікат з токена.

Робота з ключовими парами ГОСТ Р 34.10-2001
1. Для отримання декрипторов ключових пар, що зберігаються на пристрої, потрібно ввести PIN-коду. Слід розуміти, що саме значення закритого ключ отримано бути не може, так як ключ є неизвлекаемым.

var keys = Array();

try
{ 
plugin.login(deviceId, "12345678");
keys = plugin.enumerateKeys(deviceId);
}
catch (error) 
{
console.log(error);
}


2. Для генерації ключової пари потрібне введення PIN-коду. При генерації ключа параметри можуть бути обрані з набору:
  • A: id-GostR3410-2001-CryptoPro-A-ParamSet
  • B: id-GostR3410-2001-CryptoPro-B-ParamSet
  • C: id-GostR3410-2001-CryptoPro-C-ParamSet
  • XA: id-GostR3410-2001-CryptoPro-XchA-ParamSet
  • XB: id-GostR3410-2001-CryptoPro-XchB-ParamSet


Приклад генерації ключової пари ГОСТ Р 34.10-2001:
var options = {};
var keyId;

try 
{
keyId = plugin.generateKeyPair(deviceId, "A", null, options);
}
catch (error) 
{
console.log(error);
}


3. За допомогою функції deleteKeyPair ключова пара може бути видалена з токена.

Конфігурування openssl
Openssl підтримує російські криптоалгоритми, починаючи з версії 1.0. Для того, щоб їх використовувати openssl потрібно engine gost. У більшості дистрибутивів openssl ця бібліотека присутній. Щоб engine подгружалась в openssl можна прописати її в конфігураційному файлі openssl:

[openssl_def]
engines = engine_section

[engine_section]
gost = gost_section

[gost_section]
engine_id = gost
default_algorithms = ALL


Якщо конфігураційний файл openssl не розташований в стандартному місці, то шлях до нього можна задати через змінну оточення OPENSSL_CONF.

Іншим варіантом підвантаження engine gost є її передача в параметрах командного рядка утиліти openssl.

Якщо engine gost не розташована в стандартному місці, то через змінну оточення OPENSSL_ENGINES можна встановити шлях до директорії, в якій openssl буде її шукати.

Для отримання інформації про те, успішний чи був виклик утиліти openssl чи ні, з можливістю уточнення помилки, потрібно парсити stdout і stderror. В кінці статті наведено посилання на PHP-скрипт, який використовує дану утиліту.

Тепер перейдемо до реалізації закінчених користувальницьких сценаріїв.

Реєстрація на порталі

Сертифікат видається при реєстрації в системі

  • Отримуємо список підключених до комп'ютера пристроїв Рутокен ЕЦП
  • Генеруємо ключову пару ГОСТ Р 34.10-2001 на обраному Рутокен ЕЦП
  • Створюємо запит PKCS#10 на сертифікат згенерованої ключової пари
  • Відправляємо запит на сервер
  • На сервері створюємо сертифікат, прив'язуємо до аккаунту (сам сертифікат або його дескриптор). Слід зазначити, що дескриптори сертифікатів, отримані при виклику функції enumerateCertificates, є унікальними і незмінними
  • Відправляємо сертифікат на клієнт
  • На клієнті пропонуємо отриманий сертифікат
  • Імпортуємо отриманий сертифікат в Рутокен ЕЦП


Послідовність викликів в клієнтському скрипті буде наступною:



Далі запит відправляється на сервер, де на його основі видається сертифікат.
Для цього на сервері повинен бути встановлений і правильно налаштований openssl версії від 1.0 і розгорнутий функціонал УЦ.

1. Генерація улюча УЦ:
openssl genpkey-engine gost-algorithm GOST2001-pkeyopt paramset:A-out ca.key

Після цього в файлі ca.key буде створений закритий ключ

2. Створення самоподпісанного сертифіката УЦ:
openssl req-engine gost-x509-new-key ca.key-out ca.crt

Після введення необхідної інформації про видавця в файлі ca.crt буде створено сертифікат УЦ.

Отриманий від клієнта запит зберігаємо файл user.csr і видаємо на його основі сертифікат (без модифікації даних із запиту):
openssl ca-engine gost-keyfile ca.key-cert ca.crt-in user.csr-out user.crt-outform PEM-batch


Після цього в файлі user.епт створюється сертифікат користувача у форматі PEM. Його слід відправити на клієнт.
Подальша послідовність викликів на клієнта:



Сертифікат вже є на токені, виданий зовнішнім УЦ
Ключова пара при цьому повинна бути створена в форматі, сумісному з бібліотекою rtPKCS11ECP для Рутокен ЕЦП.

  • Отримуємо список підключених до комп'ютера пристроїв Рутокен ЕЦП
  • Отримуємо список всіх наявних сертифікатів користувачів на обраному Рутокен ЕЦП
  • Пропонуємо кожен сертифікат
  • Користувач вибирає потрібний сертифікат
  • Сервер формує початкову послідовність випадкових даних (рядок salt) і відправляє її на клієнт
  • Викликаємо на клієнті authenticate. При передачі salt в функцію плагіна authenticate дана послідовність доповнюється додатковими випадковими даними розміром в 32 символу, і відбувається підпис підсумкової послідовності на вибраному користувачем сертифікаті у форматі CMS attached
  • Підпис відправляється на сервер
  • На сервері відбувається перевірка CMS attached підпису з використанням кореневого сертифіката
  • З CMS attached повідомлення витягується підсумкова випадкова послідовність, від'єднується" salt і відбувається порівняння
  • Якщо порівняння успішно, то реєструємо користувача за сертифікатом, який міститься в CMS attached повідомленні


Послідовність викликів на клієнта:


Підпис виходить в 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-engine gost-verify-sign in.cms-inform PEM-CAfile ca.crt-out data.file-certsout user.crt


Тут 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


Показати DN суб'єкта (subject):
openssl x509-in cert.pem-noout-subject


Показати DN видавця:
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-a list


Сувора аутентифікація на порталі

Загальна схема автентифікації, що використовується в Рутокен Плагін, виглядає наступним чином:
  • сервер формує початкову послідовність випадкових даних (рядок salt) і відправляє її на клієнт
  • при передачі salt в функцію плагіна authenticate дана послідовність доповнюється випадковими даними розміром в 32 символу, і відбувається підпис підсумкової послідовності на вибраному користувачем сертифікаті у форматі CMS attached
  • підпис відправляється на сервер
  • на сервері відбувається перевірка підпису
  • з CMS attached повідомлення витягується підсумкова випадкова послідовність, від'єднується" salt і відбувається порівняння
  • у разі успішної перевірки користувач автентифікований на основі сертифікату, витягнутого з повідомлення CMS
Реалізація даної схеми нічим принципово не відрізняється від «Реєстрація, сертифікат вже є, виданий зовнішнім УЦ».

Електронний підпис даних та/або файлів у форматі CMS

  • формується текстове повідомлення (рядок), формування повідомлення може відбуватися як на сервері, так і на клієнті
  • якщо потрібно підписати документ довільного формату (наприклад, PDF), то потрібно перекодувати його у формат base64
  • рядок, що містить дані для підпису, передається у функцію sign
  • якщо рядок являє собою закодовані в base64 дані, то параметр функції isBase64 має бути встановлено в true, при цьому перед підписом відбудеться декодування даних з base64
  • якщо потрібно використовувати апаратне обчислення геш-функції ГОСТ Р 34.11-94 (сертифікована реалізація, швидкість 60-70 Кб/c), то в options потрібно встановити опцію useHardwareHash в true. Якщо дана опція встановлена в false, то буде використана швидка програмна реалізація геш-функції ГОСТ Р 34.11-94
  • якщо потрібно сформувати «отсоединенную» (detached) підпис CMS, то потрібно встановити опцію detached в true, інакше буде сформована «приєднана» (attached) підпис
  • для того, щоб включити/не включити користувальницький сертифікат в підписана CMS-повідомлення існує опція addUserCertificate
  • Установка опції addSignTime в true призведе до того, що в підписана CMS-повідомлення буде додано системне час як підписаного атрибута


Перевірка підпису на сервері описана вище.

Шифрування/розшифрування даних та/або файлів у форматі CMS

Шифрування даних на клієнта для сервера

Для того, щоб забезпечити конфіденційність обміну даними між клієнтом і сервером в плагіні передбачено шифрування/розшифрування даних. Дані шифруються в форматі CMS. Для того, щоб зашифрувати дані у форматі CMS, потрібно сертифікат відкритого ключа «адресата». При цьому розшифрувати таке повідомлення зможе тільки власник закритого ключа. При шифруванні даних для сервера рекомендується зберігати сертифікат сервера на Рутокен ЕЦП. Цей сертифікат може бути записана на пристрій при реєстрації користувача на порталі. Для цього слід використовувати функцію importCertificate, при цьому в якості параметра category слід передати CERT_CATEGORY_OTHER. Для використання функції cmsEncrypt потрібно отримати тіло сертифіката за його дескриптору за допомогою функції getCertificate. При цьому дескриптор є унікальним і незмінним і може бути збережений в облікового запису користувача на сервері при імпорті сертифіката сервера. Для того, щоб використовувати апаратне шифрування ГОСТ 28147-89, потрібно встановити опцію useHardwareEncryption в true. Інакше буде використана швидка програмна реалізація ГОСТ 28147-89.

Послідовність викликів наведена на малюнку:



Шифрування даних на клієнта:
var recipientCert = getCertificate(deviceId, certRecId); 
var options = {};
options.useHardwareEncryption = true;
var cms;

try
{
cms = cmsEncrypt(deviceId, certSenderId, recipientCert, data, options);
}
catch (error) 
{
console.log(error);
}


Розшифровка даних на сервері, перед расшифрованием повідомлення треба обрамити PEM-заголовками "----- BEGIN PKCS7-----" і "----- END PKCS7-----":
openssl smime-engine gost-decrypt-in message.cms-inform PEM-recіp recipient.crt-inkey recipient.key

recipient.crt — сертифікат того, для кого зашифроване повідомлення, recipient.key — ключ того, для кого зашифроване повідомлення.

Розшифровка даних, отриманих з сервера на клієнт

Для розшифрування даних, отриманих з сервера, призначена функція cmsDecrypt. Так як сервер шифрує для клієнта, використовуючи його сертифікат, то в якості keyId повинен бути переданий дескриптор закритого ключа клієнта, що відповідає відкритому ключу в сертифікаті. Цей дескриптор є унікальним і незмінним і тому може бути збережений в облікового запису користувача на сервері. Крім того, дескриптор ключа користувача може бути отриманий явним чином, шляхом виклику функції getKeyByCertificate.

Шифрування даних на сервері для клієнта:
openssl smime-encrypt-engine gost-gost89-binary-outform PEM-in data.file-out message.enc user.crt 


Розшифровка даних на клієнта:
var data;
var options = {};
try
{
data = cmsDecrypt(deviceId, keyId, cms, options);
}
catch (error) 
{
console.log(error);
}


Корисні посилання
Ці посилання можуть бути корисні розробникам инфосистем з інтеграцією ЕЦП на базі Рутокен Плагін та openssl:

Демосистема Рутокен Плагін
WEB-сервіс генерації ключів, формування запитів, управління сертифікатами, формування шаблонів запитів на сертифікати
Документація на Рутокен Плагін
Документація по використанню утиліти openssl з російськими крипталгоритмами
Приклад скрипта на PHP, який використовує утиліту openssl

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.