Цель статьи — показать возможность работы с российскими криптопровайдерами в Python под Windows через интерфейс CryptoAPI. Для этого будем использовать две библиотеки: pywin32 и PythonForWindows. Первая из них достаточно известна и нацелена на адаптацию WinAPI к использованию в Python. Вторая — относительно новый проект, позволяющий работать с функциями WinAPI так, как будто они вызываются из родного для них C, вплоть до передачи указателей. Такой подход более гибок, хотя и непривычен в программах на Python.
Для примеров использовался Python 3.12, под криптопровайдером в дальнейшим будем понимать КриптоПро CSP.
Итак, у нас есть некий файл report.zip. Поставим перед собой задачи:
-
Зашифровать файл
-
Подписать файл
-
Установить штамп времени на подпись
Шифрование
Используем пакет pywin32. Загружаем сертификат получателя из файла:
import win32crypt Store= win32crypt.CertOpenStore(CERT_STORE_PROV_FILENAME, X509_ASN_ENCODING+PKCS_7_ASN_ENCODING, None, 0, 'сертификат.cer') CertList = Store.CertEnumCertificatesInStore() Cert = CertList[0]
Параметры шифрования:
EncParam = {'ContentEncryptionAlgorithm': {'ObjId': '1.2.643.7.1.1.5.1.1', #GOST-R-34.12-2015-Magma 'Parameters': b''}, 'CryptProv': None #Криптопровайдер по умолчанию }
Шифрование:
with open('report.zip', 'rb') as MessageFile: Message = MessageFile.read() Message = win32crypt.CryptEncryptMessage(EncParam, (Cert), Message) with open('report.zip.enc', 'wb') as EncFile: EncFile.write(Message)
Примечание: если в качестве ObjId передать пустую строку, то будет выбран алгоритм шифрования по умолчанию, что правильно. А вот если передать произвольную строку, не соответствующую никаким доступным криптопровайдеру алгоритмам, то также будет выбран алгоритм по умолчанию, при этом никакой ошибки методом CryptEncryptMessage возбуждено не будет, и это совсем не правильно.
Подпись
Также используем pywin32. Получаем собственный сертификат из личного хранилища:
OurCert = None Store = win32crypt.CertOpenSystemStore('MY') CertList = Store.CertEnumCertificatesInStore() for Cert in CertList: S = bytearray(Cert.SerialNumber) S.reverse() if S.hex().upper() == 'XXX' #Серийный номер нашего сертификата OurCert = Cert break if OurCert == None: raise Exception('Не найден сертификат организации')
Параметры подписи:
SignParam = {'SigningCert': OurCert, 'HashAlgorithm': {'ObjId': '', 'Parameters': b''}, 'MsgCert': (OurCert,)}
Подпись:
with open('report.zip', 'rb') as MessageFile: Message = MessageFile.read() Message = win32crypt.CryptSignMessage(SignParam, (Message,), True #Отсоединённая подпись ) with open('report.zip.sig', 'wb') as SignFile: SignFile.write(Message)
Штамп времени
Самое интересное. Последовательность шагов здесь известна: из подписанного сообщения получаем значение подписи, передаём его в функцию CryptRetrieveTimeStamp, которая вычислит хэш и отправит его Time-Stamp сервису (TSS) по заданному адресу. Полученный от TSS ответ добавляем неподписываемым (unauthenticated) атрибутом к сообщению. Пакет pywin32 здесь не подходит, поскольку в нём отсутствуют низкоуровневые функции работы с криптографическими сообщениями (Low-level Message Functions). Поэтому будем использовать библиотеку PythonForWindows, в которой есть необходимые нам функции CryptMsgGetParam и CryptMsgControl. А вот функции CryptRetrieveTimeStamp в ней тоже нет. Обидно, досадно. Но ладно. Воспользуемся тем, что обращение к TSS идёт по незащищённому http протоколу и посмотрим, как выглядит запрос и ответ при получении штампа времени. Простой SmartSniff от Nirsoft подойдёт:
cryptcp.x64 -signf -cert -cadest -dn "Наша компания" -cadestsa http://pki.skbkontur.ru/tsp2012/tsp.srf report.zip
Любопытно, при обращении к Time-Stamp сервису СКБ Контура идёт переадресация на TSS Сертум-Про:
<head><title>Документ перемещен</title></head> <body><h1>Объект перемещен</h1>Документ теперь находится <a HREF="http://pki3.sertum-pro.ru/tsp3/tsp.srf">здесь</a></body>
поэтому штамп времени к нам будет приходить с адреса http://pki3.sertum-pro.ru/tsp3/tsp.srf. Самый первый пакет в адрес crl1.ca.cbr.ru — это OCSP (Online Certificate Status Protocol) запрос статуса нашего сертификата при его использовании для подписи документа, в данный момент для нас он не имеет значения. Итак, смотрим третий пакет. Разумно предположить, что сообщения при обмене криптографической информацией передаются в кодировке CER (DER). Посмотрим на содержимое запроса в любом ASN.1 декодере:
Пришло время обратиться к первоисточникам, а именно к спецификации Time-Stamp протокола RFC3161, где описан формат запроса:
TimeStampReq ::= SEQUENCE { version INTEGER { v1(1) }, messageImprint MessageImprint, reqPolicy TSAPolicyId OPTIONAL, nonce INTEGER OPTIONAL, certReq BOOLEAN DEFAULT FALSE, extensions [0] IMPLICIT Extensions OPTIONAL } MessageImprint ::= SEQUENCE { hashAlgorithm AlgorithmIdentifier, hashedMessage OCTET STRING }
Очевидно, version — это INTEGER 1; hashAlgorithm — OBJECT IDENTIFIER 1.2.643.7.1.1.2.2; hashedMessage — OCTET STRING XXX. reqPolicy — политика TSA (Time Stamping Authority), в соответствии с которой должен быть указан TimeStampToken — не передаётся, как и extensions — расширения. certReq — наше BOOLEAN True — определяет, будет ли в ответном сообщении присутствовать сертификат TSS. Наконец, поле nonce — длинное целое — служит для уникальной идентификации запроса и может быть произвольным, например, представленным восемью случайными байтами:
import random def GetNonce(): IntList = [] for i in range(8): IntList.append(random.randint(0, 255)) return bytes(IntList)
Для Python существуют пакеты для работы с ASN.1 кодировками, но структура запроса (да и ответа) вполне проста, и сторонние средства нам не понадобятся. Однако, чтобы составить запрос, нужно сперва получить хэш электронной подписи (сама подпись — тоже хэш, только зашифрованный), для которого и устанавливается метка времени.
from windows.crypto import * from windows import winproxy from windows.generated_def import * with open('report.zip.sig','rb') as SignFile: Message=SignFile.read() #Загружаем сообщение. Здесь неявно вызываются функции CryptMsgOpenToDecode и CryptMsgUpdate hMsg = windows.crypto.CryptMessage.from_buffer(Message) #Содержимое подписи Sign = bytes(hMsg.get_signer_data().EncryptedHash.data) cbData = DWORD() #Получаем контекст криптопровайдера, обёртка для CryptAcquireContextW with windows.crypto.CryptContext(dwProvType = 80, dwFlags = CRYPT_VERIFYCONTEXT #Не нужно открывать контейнер ) as Prov: hHash = HCRYPTHASH() #Создаём объект хэша winproxy.CryptCreateHash(hProv=Prov,Algid=32801,hKey=None,dwFlags=0,phHash=hHash) #Хэшируем подпись winproxy.CryptHashData(hHash, Sign) #Получаем длину хэша winproxy.CryptGetHashParam(hHash, HP_HASHVAL, None, cbData) HASH = create_string_buffer(cbData.value) #Получаем хэш winproxy.CryptGetHashParam(hHash, HP_HASHVAL, PBYTE(HASH), cbData)
Константы dwProvType=80 и Algid=32801 определяются конкретным криптопровайдером. В случае КриптоПро это выглядит так:
На примере вызова CryptGetHashParam видим знакомый способ работы со многими функциями CryptoAPI: сначала вызываем функцию, чтобы получить длину данных (cbData) и выделить под них память, затем вызываем её же, чтобы получить данные (HASH).
Итак, мы получили хэш подписи, теперь можем составить запрос к TSS:
Request = b'\x30\x40\x02\x01\x01\x30\x2E\x30\x0A\x06\x08\x2A\x85\x03\x07\x01\x01\x02\x02\x04\x20' + bytes(HASH) + b'\x02\x08' + GetNonce() + b'\x01\x01\xFF'
Посылаем:
import requests Header = {'Content-Type': 'application/timestamp-query', 'Connection': 'Keep-Alive', 'Content-Length': '66', 'User-Agent': 'requests'} Session = requests.Session() Response = Session.post('http://pki.skbkontur.ru/tsp2012/tsp.srf', Request, headers = Header) Session.close()
Посмотрим, что вернулось к нам в Response.content:
Формат ответа по спецификации:
TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken TimeStampToken OPTIONAL }
Интересующий нас timeStampToken — последовательность (SEQUENCE), озаглавленная OID 1.2.840.113549.1.7.2. Найдём её в двоичной строке:
Idx = Response.content.find(b'\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x07\x02') - 4
Здесь мы делаем разумное допущение, что длина последовательности, содержащей атрибут времени, его подпись и сертификат подписанта, лежит в пределах от 128 до 65 535 байт, поэтому её заголовок занимает 4 байта.
Добавим timeStampToken к сообщению как неподписываемый атрибут. Нам понадобится структура CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR_PARA(), которая не объявлена в пакете, поэтому объявим её сами:
class CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR_PARA(Structure): _fields_ = [ ("cbSize",DWORD), ("dwSignerIndex",DWORD), ("blob",CRYPT_DATA_BLOB), ] #Подготовка атрибута Attr = CRYPT_ATTRIBUTE() Attr.pszObjId = '1.2.840.113549.1.9.16.2.14'.encode("ascii") #OID timeStampToken Attr.cValue = 1 Attr.rgValue = PCRYPT_INTEGER_BLOB(CRYPT_INTEGER_BLOB.from_string(Response.content[Idx:])) #Упаковка его в ASN.1 структуру winproxy.CryptEncodeObjectEx(DEFAULT_ENCODING,PKCS_ATTRIBUTE,byref(Attr),0,None,None,cbData) Buf = create_string_buffer(cbData.value) winproxy.CryptEncodeObjectEx(DEFAULT_ENCODING,PKCS_ATTRIBUTE,byref(Attr),0,None,Buf,cbData) #Добавление к сообщению Param = CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR_PARA() Param.cbSize = sizeof(Param) Param.blob = CRYPT_DATA_BLOB.from_string(Buf) winproxy.CryptMsgControl(hMsg, 0, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, byref(Param))
Штамп времени установлен. Осталось сохранить обновлённое сообщение:
winproxy.CryptMsgGetParam(hMsg, CMSG_ENCODED_MESSAGE, 0, None, cbData); Buf = create_string_buffer(cbData.value) winproxy.CryptMsgGetParam(hMsg, CMSG_ENCODED_MESSAGE, 0, Buf, cbData) with open('report.zip.sig', 'wb') as SignFile: SignFile.write(Buf)
Мы получили файл report.zip.sig, содержащий подпись и штамп времени:
Сертификат Time-Stamp сервиса Сертум-Про выдан на физическое лицо. Я не знаю, кто этот достойный человек, но отдадим должное его благородному труду.
Замечание по поводу TSS Центробанка: для обращения к нему требуется tls-туннель, но на уровне пользователя это никак не отражается, и вышеприведённый код по-прежнему работает.
ссылка на оригинал статьи https://habr.com/ru/articles/834312/
Добавить комментарий