PYтокен: история о том, как питон съел ЭЦП. Часть 2

от автора

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