Привет, Хабр! Это продолжение статьи про задание по программированию «сделать программу, с помощью которой можно будет подписывать ЭЦП и проверять её». В прошлой статье я уже реализовал это с помощью обычной флешки, но вспомнил про программно-аппаратные модули и выбрал Рутокен ЭЦП 3.0, на это есть несколько причин:
были скидки);
в будущем планирую использовать для двухфакторной аутентификации;
имеет не только программную реализацию криптоалгоритмов, но и аппаратную(создание ключей непосредственно на самом USB-токене).
Итак, данное устройство поддерживает сертификаты и ключи, отличные от уже написанной программы, и значит, следует переписать код для реализации подписания и проверки файлов.
Первоочередная задача — это установка драйверов и библиотеки PyKCS11 с официального сайта.
Перед началом написания программы следует:
настроить токен, поменять pin-код для пользователя и администратора(по умолчанию 12345678-пользователь, 87654321-администратор);
сгенерировать сертификат с ключами и импортировать их на флешку.
После выполнения данных действий можно приступить к написанию программы.
Инициализация пути до папки библиотеки:
def __init__(self):
self.pkcs11 = PyKCS11Lib()
try:
self.pkcs11.load('C:/.../rtPKCS11ECP.dll')
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить библиотеку Рутокен: {str(e)}")
self.session = None
Самое важное при инициализации подключённого токена — это ввод пин-кода и завершение сессии работы с ней после каждого взаимодействия, в противном случае, если это не реализовать, то при попытке осуществить несколько действий (подписание и проверка подписи) за один запуск программы, будет отображаться ошибка, что кто-то уже залогинился на неё:
def connect(self, pin=None):
try:
if self.session:
self.disconnect()
if pin is None:
pin = self.request_pin()
if pin is None:
return False
slots = self.pkcs11.getSlotList()
if not slots:
raise Exception("Не найдены доступные слоты Рутокен")
self.session = self.pkcs11.openSession(slots[0])
self.session.login(pin)
return True
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось подключиться к Рутокен: {str(e)}")
return False
def disconnect(self):
try:
if self.session:
self.session.logout()
self.session.closeSession()
self.session = None
except Exception:
pass
def request_pin(self):
root = tk.Tk()
root.withdraw()
pin = simpledialog.askstring(
"PIN-код токена",
"Введите PIN-код для доступа к USB-токену:",
show='*'
)
root.destroy()
return pin
Поиск закрытого ключа и сертификата(в нём расположен и открытый ключ) на токене:
def find_gost_keys(self):
try:
priv_key = self.session.findObjects([
(CKA_CLASS, CKO_PRIVATE_KEY),
(CKA_KEY_TYPE, CKK_GOSTR3410)
])
if not priv_key:
raise Exception("На токене не найден закрытый ключ ГОСТ")
certs = self.session.findObjects([
(CKA_CLASS, CKO_CERTIFICATE)
])
if not certs:
raise Exception("На токене не найден сертификат")
return priv_key[0], certs[0]
except Exception as e:
messagebox.showerror("Ошибка", f"Ошибка поиска ключей ГОСТ: {str(e)}")
return None, None
Создание хэша ГОСТ Р 34.11 и подписи ГОСТ Р 34.10:
def gost_sign(self, data):
try:
priv_key, _ = self.find_gost_keys()
if not priv_key:
return None
digest = self.session.digest(data, Mechanism(CKM_GOSTR3411, None))
signature = self.session.sign(priv_key, digest, Mechanism(CKM_GOSTR3410, None))
return bytes(signature)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось подписать: {str(e)}")
return None
Подключение и создание подписи с последующим сохранением в файл(.sig):
def sign_file(self, file_path):
try:
pin = self.rutoken.request_pin()
if pin is None:
return False
if not self.rutoken.connect(pin):
return False
with open(file_path, "rb") as f:
data = f.read()
signature = self.rutoken.gost_sign(data)
self.rutoken.disconnect()
if not signature:
return False
signature_path = file_path + ".sig"
with open(signature_path, "wb") as f:
f.write(signature)
self.log(f"Файл подписан: {os.path.basename(file_path)}")
return True
except Exception as e:
self.log(f"Ошибка подписи: {str(e)}")
return False
Проверка файла с помощью подписи:
def verify_signature(self, file_path, signature_path):
try:
pin = self.rutoken.request_pin()
if pin is None:
return False
if not self.rutoken.connect(pin):
return False
_, cert = self.rutoken.find_gost_keys()
if not cert:
self.rutoken.disconnect()
return False
with open(file_path, "rb") as f:
data = f.read()
with open(signature_path, "rb") as f:
signature = f.read()
digest = self.rutoken.session.digest(data, Mechanism(CKM_GOSTR3411, None))
pub_key = self.rutoken.session.findObjects([(CKA_CLASS, CKO_PUBLIC_KEY)])[0]
result = self.rutoken.session.verify(
pub_key,
digest,
signature,
Mechanism(CKM_GOSTR3410, None)
)
self.rutoken.disconnect()
return bool(result)
except Exception as e:
self.log(f"Ошибка проверки: {str(e)}")
return False
Шифрование и расшифрование директорий будет осуществляться с помощью ключа, который сгенерирован на основе введённого pin-кода и серийного номера:
def generate_key_from_token(self, pin=None):
try:
if not self.connect(pin):
return None
slot_list = self.pkcs11.getSlotList()
if not slot_list:
raise Exception("Не найдены доступные слоты Рутокен")
token_info = self.pkcs11.getTokenInfo(slot_list[0])
serial = token_info.serialNumber.strip()
if pin is None:
pin = self.request_pin()
if not pin:
return None
key_material = f"{serial}{pin}".encode()
salt = hashlib.sha256(serial.encode()).digest()
kdf = PBKDF2HMAC(
algorithm=hashes.SHA512(),
length=32,
salt=salt,
iterations=1000000,
backend=default_backend()
)
key = base64.urlsafe_b64encode(kdf.derive(key_material))
return key
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сгенерировать ключ: {str(e)}")
return None
finally:
self.disconnect()
Я понимаю, что это плохая идея, однако другим способом не получилось реализовать, при попытке использования ключа шифрования с Рутокена пишет ошибку «секретный ключ не найден», посмотрев подобные проблемы на сайте технической поддержки продукта, был найден ответ, однако реализация с полученными рекомендациями мне не совсем ясна и поэтому реализована сейчас не будет. Полный код опубликован на моём GitHub. Возможно, при появлении большего количества примеров реализации данной функции я изменю код, также буду рад предложениям по улучшению кода в комментариях.

А пока буду сам изучать информацию и, возможно, сделаю часть 3 или добавлю в эту статью изменённый код с улучшениями. Спасибо за поддержку и большую активность под прошлой статьёй.