Привет, Хабр! Это продолжение статьи про задание по программированию «сделать программу, с помощью которой можно будет подписывать ЭЦП и проверять её». В прошлой статье я уже реализовал это с помощью обычной флешки, но вспомнил про программно-аппаратные модули и выбрал Рутокен ЭЦП 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 или добавлю в эту статью изменённый код с улучшениями. Спасибо за поддержку и большую активность под прошлой статьёй.
ссылка на оригинал статьи https://habr.com/ru/articles/914720/
Добавить комментарий