Полная цепочка взаимодействия: Рутокен ЭЦП 2.0 + rtPKCS11ECP.dll + КриптоПро + .NET
Ниже подробно описана архитектура, потоки данных и взаимодействие всех компонентов при подписании данных с использованием Рутокен ЭЦП 2.0.
Общая архитектура (схема)
┌─────────────────────────────────────────────────────┐
│ Ваше .NET-приложение (RutokenSigner) │
│ • Pkcs11Interop 5.3.0 (HighLevelAPI) │
│ • BouncyCastle.Cryptography 2.6.2 (хеширование) │
│ • System.Text.Json (формирование JSON) │
└──────────────┬──────────────────────────────────────┘
│ вызовы через интерфейс ISession
▼
┌─────────────────────────────────────────────────────┐
│ PKCS#11 Middleware: rtPKCS11ECP.dll │
│ • Реализация стандарта PKCS#11 v2.40 │
│ • Vendor-расширения ТК-26 (0xd43210xx) │
│ • Маршрутизация команд к драйверу токена │
│ • Преобразование .NET-типов ↔ нативные структуры │
└──────────────┬──────────────────────────────────────┘
│ вызовы через C API (C_SignInit, C_Sign...)
▼
┌─────────────────────────────────────────────────────┐
│ Драйвер уровня ядра: rtDevice.sys / librt*.so │
│ • Управление USB-устройством │
│ • Буферизация команд и ответов │
│ • Шифрование канала связи с токеном (опционально) │
└──────────────┬──────────────────────────────────────┘
│ протокол обмена с чипом
▼
┌─────────────────────────────────────────────────────┐
│ 🔐 Аппаратный токен: Рутокен ЭЦП 2.0 │
│ • Защищённый микроконтроллер (SmartCard chip) │
│ • Невыводимые закрытые ключи (внутри чипа) │
│ • Аппаратная реализация ГОСТ-алгоритмов: │
│ - ГОСТ Р 34.11-2012 (хеширование) │
│ - ГОСТ Р 34.10-2012 (подпись) │
│ • Защищённая память для сертификатов и ключей │
└─────────────────────────────────────────────────────┘
Детальная цепочка вызовов при подписании
Шаг 1: Загрузка PKCS#11 библиотеки
// Ваше приложение
var factories = new Pkcs11InteropFactories();
using IPkcs11Library pkcs11 = factories.Pkcs11LibraryFactory
.LoadPkcs11Library(factories, "rtPKCS11ECP.dll", AppType.MultiThreaded);Что происходит:
LoadPkcs11Library()вызываетLoadLibrary()(Windows) /dlopen()(Linux)- Загружается
rtPKCS11ECP.dllв адресное пространство процесса - Вызывается
C_Initialize()— инициализация внутреннего состояния библиотеки - Создаётся объект
IPkcs11Library, который является обёрткой над нативными функциями
Шаг 2: Поиск слотов и токенов
var slots = pkcs11.GetSlotList(SlotsType.WithTokenPresent);Поток данных:
.NET → Pkcs11Interop → rtPKCS11ECP.dll → rtDevice.sys → USB-стек → Рутокен
↓
.NET ← Pkcs11Interop ← rtPKCS11ECP.dll ← rtDevice.sys ← USB-стек ← Рутокен
Что возвращается:
- Список слотов (физических/виртуальных портов)
- Для каждого слота:
CK_TOKEN_INFO(метка, серийный номер, версия прошивки)
Шаг 3: Открытие сессии
using ISession session = slots[0].OpenSession(SessionType.ReadWrite);Что происходит внутри rtPKCS11ECP.dll:
- Вызов
C_OpenSession(slotID, CKF_RW_SESSION, ...) - Библиотека проверяет, что токен в слоте доступен
- Создаётся контекст сессии (хранит состояние: логин, активные операции)
- Устанавливается соединение с токеном через драйвер
Шаг 4: Аутентификация (ввод PIN)
session.Login(CKU.CKU_USER, "12345678");Поток выполнения:
1. .NET: session.Login()
2. Pkcs11Interop: преобразует строку PIN в byte[] + вызывает C_Login()
3. rtPKCS11ECP.dll:
• Проверяет формат PIN (длина, кодировка)
• Формирует APDU-команду VERIFY для токена
• Отправляет через драйвер
4. Рутокен:
• Принимает PIN
• Сравнивает с эталоном в защищённой памяти
• Возвращает статус (успех/ошибка)
5. При успехе: в сессии выставляется флаг "авторизован"⚠️ Важно: После 3-5 неверных попыток токен блокируется (зависит от настроек).
Шаг 5: Поиск ключа для подписи
var keyFilter = new List<IObjectAttribute>
{
f.ObjectAttributeFactory.Create(CKA.CKA_CLASS, CKO.CKO_PRIVATE_KEY),
f.ObjectAttributeFactory.Create(CKA.CKA_ID, certId),
f.ObjectAttributeFactory.Create(CKA.CKA_SIGN, true)
};
var keys = session.FindAllObjects(keyFilter);Как работает поиск на токене:
1. Pkcs11Interop формирует шаблон поиска (template)
2. rtPKCS11ECP.dll преобразует его в внутренние структуры
3. Вызывается C_FindObjectsInit() → C_FindObjects() → C_FindObjectsFinal()
4. Рутокен сканирует свою память объектов:
• Каждый объект имеет тип (ключ, сертификат, данные)
• У каждого объекта есть атрибуты (CKA_*)
• Поиск идёт по точному совпадению указанных атрибутов
5. Возвращаются дескрипторы (handles) найденных объектовСтруктура объекта ключа на Рутокене:
┌─────────────────────────────┐
│ Объект: закрытый ключ │
├─────────────────────────────┤
│ • CKA_CLASS = CKO_PRIVATE_KEY
│ • CKA_KEY_TYPE = CKK_GOSTR3410_2012_256
│ • CKA_ID = [A1 B2 C3 D4] ← связка с сертификатом
│ • CKA_LABEL = "Key for signing"
│ • CKA_SIGN = true ← можно подписывать
│ • CKA_EXTRACTABLE = false ← ключ нельзя извлечь!
│ • Ключевые данные: [ЗАШИФРОВАНЫ, внутри чипа]
└─────────────────────────────┘
Шаг 6: Хеширование данных (BouncyCastle)
byte[] hash = ComputeGostHash(data); // Gost3411_2012_256DigestПочему хешируем на стороне приложения?
Вариант | Где хешируется | Плюсы | Минусы |
|---|---|---|---|
На ПК (BouncyCastle) | В .NET-приложении | • Быстро • Не зависит от токена • Легко отлаживать | • Не сертифицировано для юр. значимости |
На токене | Внутри Рутокена | • Сертифицировано • Ключ никогда не покидает токен | • Медленнее • Требует механизма с хешированием |
Ваш код использует первый вариант — это нормально для тестов и внутренних систем.
Шаг 7: Подпись на токене
IMechanism mech = factories.MechanismFactory.Create((CKM)0xd4321006UL);
byte[] signature = session.Sign(mech, privateKey, hash);Детальный поток выполнения:
1. .NET: session.Sign(mech, keyHandle, hash)
2. Pkcs11Interop:
• Преобразует IMechanism в CK_MECHANISM (нативная структура)
• Преобразует IObjectHandle в CK_OBJECT_HANDLE (uint)
• Вызывает C_SignInit() + C_Sign()
3. rtPKCS11ECP.dll:
• Проверяет, что механизм 0xd4321006 поддерживается
• Проверяет, что ключ имеет CKA_SIGN=true
• Формирует команду для токена:
┌─────────────────────────────┐
│ Команда: SIGN │
│ • Механизм: 0xd4321006 │
│ • Дескриптор ключа: 0x1234 │
│ • Данные: [32 байта хеша] │
└─────────────────────────────┘
4. Рутокен (внутри чипа):
• Находит ключ по дескриптору в защищённой памяти
• Загружает ключевые параметры в крипто-движок
• Выполняет алгоритм ГОСТ Р 34.10-2012:
- Вход: 32-байтный хеш
- Выход: 64-байтная подпись (R || S, по 32 байта)
• Возвращает подпись
5. rtPKCS11ECP.dll → Pkcs11Interop → .NET: byte[64]Важно: Закрытый ключ никогда не покидает токен. На ПК передаётся только хеш и возвращается только подпись.
Шаг 8: Завершение сессии
session.Logout(); // Вызывает C_Logout()
// using-блок вызывает Dispose() → C_CloseSession() → C_Finalize()Что очищается:
- Сбрасывается флаг авторизации в сессии
- Освобождаются дескрипторы объектов
- Закрывается соединение с токеном
- При
C_Finalize()— освобождение ресурсов библиотеки
🔐 Где и как хранятся ключи на Рутокене
┌─────────────────────────────────────┐
│ Защищённая память Рутокен ЭЦП 2.0 │
├─────────────────────────────────────┤
│ │
│ ┌──────────────────────────────┐ │
│ │ Область: Закрытые ключи │ │
│ │ • Доступ: только после PIN │ │
│ │ • Экспорт: запрещён │ │
│ │ • Формат: PKCS#8 + шифрование│ │
│ └──────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ Область: Сертификаты │ │
│ │ • Доступ: публичный │ │
│ │ • Формат: DER (X.509) │ │
│ │ • Связка: по CKA_ID │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ Область: Пользовательские │ │
│ │ данные (опционально) │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────┘Связка "сертификат ↔ ключ":
- Оба объекта имеют одинаковый атрибут
CKA_ID(байтовый массив) - При поиске ключа вы сначала находите сертификат, берёте его
CKA_ID, ищете ключ с таким жеCKA_ID - Это стандарт PKCS#11, работает на всех токенах
🔄 Взаимодействие с КриптоПро CSP
Если на системе установлен КриптоПро CSP, возможны два сценария:
Сценарий А: КриптоПро НЕ используется (только Рутокен)
Приложение → rtPKCS11ECP.dll → РутокенСценарий Б: КриптоПро использует Рутокен как хранилище ключей
Приложение → CryptoPro CSP → rtPKCS11ECP.dll → РутокенКак это работает:
- КриптоПро регистрирует свой PKCS#11-модуль (
cryptopki.dll) - При создании контейнера КриптоПро может выбрать Рутокен как целевое устройство
- КриптоПро вызывает функции
rtPKCS11ECP.dllдля:- Создания ключей (
C_GenerateKeyPair) - Записи сертификатов (
C_CreateObject) - Подписи (
C_Sign)
- Создания ключей (
- Ваш .NET-код может работать напрямую с rtPKCS11ECP.dll, минуя КриптоПро
⚠️ Важно: rtPKCS11ECP.dll — это библиотека Рутокена, а не КриптоПро.
КриптоПро имеет свою библиотеку: cryptopki.dll (Windows) / libcryptoki.so (Linux).
Диагностика: как проверить каждый уровень
1. Проверка USB-подключения
# Windows
Get-PnpDevice | Where-Object {$_.FriendlyName -like "*Rutoken*"}
# Linux
lsusb | grep -i rutoken
dmesg | grep -i usb2. Проверка драйвера и библиотеки
# Файл библиотеки
Test-Path "C:\Windows\System32\rtPKCS11ECP.dll"
# Версия драйвера (через реестр)
Get-ItemProperty "HKLM:\SOFTWARE\Aktiv Co.\Rutoken" | Select-Object Version3. Проверка PKCS#11-интерфейса
# Утилита pkcs11-tool (из пакета opensc или Pkcs11Interop)
pkcs11-tool --module rtPKCS11ECP.dll --list-slots
pkcs11-tool --module rtPKCS11ECP.dll --list-mechanisms | findstr /i "gost"4. Проверка токена из .NET (минимальный тест)
var f = new Pkcs11InteropFactories();
using var lib = f.Pkcs11LibraryFactory.LoadPkcs11Library(f, "rtPKCS11ECP.dll", AppType.MultiThreaded);
var slots = lib.GetSlotList(SlotsType.WithTokenPresent);
Console.WriteLine($"Слотов: {slots.Count}");
if (slots.Count > 0)
{
var info = slots[0].GetTokenInfo();
Console.WriteLine($"Метка: {info.Label}");
Console.WriteLine($"Серийный: {info.SerialNumber}");
}🚨 Типичные проблемы и решения
Проблема | Уровень | Причина | Решение |
|---|---|---|---|
| Загрузка DLL | Библиотека не найдена | Установить драйверы Рутокен или задать путь через |
| Драйвер/токен | Токен не отвечает | Переподключить токен, проверить USB-порт, обновить драйвер |
| Аутентификация | Неверный PIN | Ввести правильный (по умолчанию: |
| Механизм | Токен не поддерживает | Использовать |
| Поиск ключа | Дескриптор устарел | Пересоздать сессию, не хранить handles между сессиями |
| Авторизация | Попытка использовать ключ без логина | Вызвать |
Подпись не верифицируется | Криптография | Передан хеш вместо данных (или наоборот) | Для механизма |
Сравнение: где что выполняется
Операция | Где выполняется | Почему так |
|---|---|---|
Формирование JSON | .NET-приложение | Бизнес-логика, не связана с крипто |
Хеширование (ГОСТ) | .NET (BouncyCastle) | Быстрее, проще отлаживать; для юр. значимости — на токене |
Поиск ключа | Рутокен | Ключи хранятся внутри, поиск по защищённой памяти |
Подпись | Рутокен (аппаратно) | Закрытый ключ не должен покидать токен (требование безопасности) |
Проверка подписи | .NET или внешняя система | Публичный ключ доступен всем, проверка не требует токена |
Безопасность: что защищено, что нет
Компонент | Защищённость | Комментарий |
|---|---|---|
Закрытый ключ | 🔒 Максимальная | Никогда не покидает чип токена, неэкстрагируемый |
PIN-код | 🔒 Высокая | Передаётся по зашифрованному каналу, не логируется |
Хеш данных | ⚠️ Средняя | Передаётся в открытом виде (но это не секрет) |
Подпись | ✅ Публичная | Предназначена для передачи и проверки |
Исходные данные | ⚠️ Зависит от приложения | Шифруйте канал передачи, если данные конфиденциальны |
Сессия PKCS#11 | 🔒 Высокая | Дескрипторы валидны только в рамках сессии |
Рекомендации по архитектуре
Для производства:
// 1. Вынесите настройки токена в конфиг
public static class TokenSettings
{
public static string LibraryPath { get; set; } =
Environment.GetEnvironmentVariable("TOKEN_PKCS11")
?? (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? @"C:\Windows\System32\rtPKCS11ECP.dll"
: "/usr/lib/librtpkcs11ecp.so");
public static ulong PreferredMechanism { get; set; } = 0xd4321006UL;
public static bool UseTokenForHashing { get; set; } = false; // true для юр. значимости
}
// 2. Добавьте логирование операций
using var logger = LoggerFactory.Create(b => b.AddConsole()).CreateLogger<Program>();
logger.LogInformation("Загрузка библиотеки: {Path}", TokenSettings.LibraryPath);
// 3. Обрабатывайте все исключения специфично
catch (Pkcs11Exception ex) when (ex.RV == CKR.CKR_MECHANISM_INVALID)
{
logger.LogWarning("Механизм {Mech} не поддерживается, пробуем альтернативы...", SIGN_MECHANISM);
// Попробовать другой механизм
}Для юридически значимой подписи:
- Используйте хеширование на токене (механизм
0xd4321008) - Замените BouncyCastle на сертифицированный провайдер (КриптоПро)
- Оберните подпись в CMS/PKCS#7 с указанием всех атрибутов
- Ведите журнал операций (кто, когда, что подписал)
Итоговая схема потока данных при подписании
[.NET-приложение]
│
▼
[Pkcs11Interop: HighLevelAPI]
│ • Преобразование типов
│ • Управление сессиями
▼
[rtPKCS11ECP.dll]
│ • Реализация PKCS#11 v2.40
│ • Vendor-расширения ТК-26
│ • Маршрутизация к драйверу
▼
[rtDevice.sys / librt*.so]
│ • USB-коммуникация
│ • Буферизация, таймауты
▼
[Рутокен ЭЦП 2.0 (чип)]
│ • Защищённая память
│ • Аппаратный ГОСТ-движок
│ • Невыводимые ключи
▼
[Возврат: 64-байтная подпись]
│
▼
[.NET-приложение: формирование итогового JSON]
💡 Ключевой вывод: Ваш код корректно реализует всю цепочку. Главное — убедиться, что:
- Драйверы и библиотека установлены
- Механизм
0xd4321006 поддерживается вашей прошивкой - Ключ на токене имеет
CKA_SIGN=true и правильный тип (CKK_GOSTR3410_2012_256)
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.