Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров2.2K

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

Теги:
Хабы:
+1
Комментарии2

Публикации

Работа

Data Scientist
56 вакансий

Ближайшие события