В один прекрасный день где-то кто-то  поменял законодательство и нашей курьерской компании стало нужно передавать данные о доставленных ювелирных изделиях в систему ГИИС ДМДК (это государственная система контроля за оборотом драгоценных металлов и камней). Доступ к ГИИС ДМДК предоставляется в виде SOAP сервиса, куда необходимо передавать подписанные запросы, т. е. подписанные XML-документы. Одним из требований к интеграции было использование КриптоПро для построения защищенного канала передачи данных. Поскольку КриптоПро уже присутствовал в проекте, это сделало очевидным выбор данного продукта и для подписи передаваемых документов.

В этой статье я расскажу о том, как мы реализовали подпись XML-документов для передачи в ГИИС ДМДК с помощью расширения КриптоПро phpсades, какие подводные камни встретились по пути и как их удалось обойти. 

На чем все написано

Мы использовали  в проекте  php 8.3 и фреймворк Laravel, установленные на Ubuntu Desktop. Также мы использовали СКЗИ «КриптоПро CSP» версии 5.0 R3 и нам потребовалось собрать и установить расширение phpсades для работы с КриптоПро из php.

Выбор десктопной версии Ubuntu объясняется тем, что мы хотели использовать клиентскую лицензию Крипто Про (она сильно дешевле серверной), но на серверную ОС клиентская лицензия не ставится. Бонусом добавилась возможность работать с сертификатами при помощи графического интерфейса КриптоПро, что намного удобней, чем работа из командной строки

Что было сложно

Основной проблемой в проекте была недостаточная документация. По сути на сайте Криптопро есть два документа, касающихся разработки под php: это инструкция по сборке расширения  и один(!) пример использования. Со всеми остальными вопросами разработчиков направляют  к документации для браузерного плагина с пояснением, что там реализованы аналогичные методы. 

К этому добавляются абсолютно неинформативные сообщения об ошибках. Вот один из примеров: у вас не работает верификация подписи. Угадайте почему


Из-за отсутствия нормальной документации приходится тратить огромное количество времени на поиски информации на форумах и ее анализ. 

Это цитата с форума хорошо передает чувства человека, который в первый раз пишет что-то с КриптоПро



Чтобы немного поправить ситуацию и была написана эта статья

Как устроена подпись XML документа

Для тех, кто раньше не сталкивался с XML-подписями, расскажу простыми словами, как они устроены. Подписывается содержимое какого-то узла в XML. Есть несколько видов XML подписей и различаются они тем, как относительно друг друга размещаются подписываемые данные и подпись  

  1. Отсоединенная подпись (Detached signature) — используется для подписи данных, находящихся вне узла  с подписью. Подпись и подписываемые данные могут быть в одном документе, могут быть в разных документах, при этом подпись ссылается на подписываемый ресурс через URI. 

  2. Включающая подпись (Enveloping signature) — подпись содержит подписываемые данные внутри себя. В этом случае подпись является корневым элементом документа, а подписываемые данные размещаются внутри элемента 

  3. Включенная подпись (Enveloped signature) — подпись является частью подписываемого документа. В этом случае элемент подписи находится внутри подписываемого XML-документа. 

Перед подписанием, данные нужно каноникализировать, т.е. привести XML-данные к стандартизированному виду согласно определённым правилам. 

Далее от данных считается дайджест, по дайджесту формируется подпись и узел с подписью добавляется в xml-документ

В простом случае узел с подписью выглядит примерно так

<ds:Signature
	xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="id элемента подписи">
	<ds:SignedInfo>
		<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod>
		<ds:SignatureMethod Algorithm="алгоритм создания подписи"></ds:SignatureMethod>
		<ds:Reference URI="ссылка на элемент, содержащий подписываемые данные">
			<ds:Transforms>
				<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform>
				<ds:Transforms>
					<ds:Transform Algorithm="дополнительный алгоритм трансформации"></ds:Transform>
				</ds:Transforms>
				<ds:DigestMethod Algorithm="метод по которому считается дайджест"></ds:DigestMethod>
				<ds:DigestValue>дайджест подписываемых данных</ds:DigestValue>
			</ds:Reference>
		</ds:SignedInfo>
		<ds:SignatureValue>подпись</ds:SignatureValue>
		<ds:KeyInfo>
			<ds:X509Data>
				<ds:X509Certificate>ключ</ds:X509Certificate>
			</ds:X509Data>
		</ds:KeyInfo>
	</ds:Signature>

В нашем случае надо было реализовать Detached Signature и кроме обычной каноникализации применить к данным еще и smev-transform (https://info.gosuslugi.ru/articles/%D0%90%D0%9B%D0%93%D0%9E%D0%A0%D0%98%D0%A2%D0%9C_%D1%82%D1%80%D0%B0%D0%BD%D1%81%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%86%D0%B8%D0%B8_XML/)

Задача подписи XML-документа без дополнительных трансформаций решается стандартными классами КриптоПро в несколько строчек

$sContent = “Содержимое подписываемого файла XML”;

$sd = new CPSignedXml();

$sd->set_SignatureType(2);   //CADESCOM_XML_SIGNATURE_TYPE_TEMPLATE = 2  (Подпись по шаблону)

$sd->set_Content($sContent);

$signedXML = $sd->Sign($signer,null);

Если надо применять дополнительные трансформы, алгоритм создания подписи немного сложней.

  1. Получаем строку с каноникализированным значением подписываемого узла

  2. Далее применяем нужные трансформы к этой строке (каждый новый трансформ получает на вход результат работы предыдущего). На выходе у нас получается строка, содержащая трансформированный XML

  3. Считаем дайджест для полученной строки (для подсчета используем алгоритм, указанный в DigestMethod)

  4. Записываем значение дайджеста в элемент DigestValue подписи

  5. Каноникализируем значение узла SignedInfo подписи

  6. Подписываем элемент SignedInfo. Для этого используем классы CPHashedData и CPRawSignature. 

$signer = new \CPSigner();

$signer->set_Certificate($this->certificate); // задаем используемый сертификат 

// Создаем объект для хеширования

$hasher = new \CPHashedData();

// Устанавливаем алгоритм хеширования, указанный в узле <DigestMethod>

$hasher->set_Algorithm($this->hashAlgorithm); // используем алгоритм хэширования, указанный в сертификате

$hasher->set_DataEncoding(BASE64_TO_BINARY);

// Хешируем данные

$hasher->Hash(base64_encode($signedString));

$cr = new \CPRawSignature();

// получаем подпись

$rawSignature = $cr->SignHash($hasher, $this->certificate);

// Дальше надо перевернуть подпись 

$binSignature = hex2bin($rawSignature);

$reversedSignature = strrev($binSignature);

// И закодировать ее в base64

$encodedSignature = base64_encode($reversedSignature);

7. После этого записываем подпись в узел SignatureValue

8. Записываем публичный ключ в узел KeyInfo

9. И каноникализируем полученный XML с подписью

Вот и все. Пусть эти несколько строчек кода помогут вам сэкономить время для чего-то более приятного, чем исследование форумов КриптоПро. Вот тут можно посмотреть полный код тестового сервиса для подписи

https://gitlab.com/marinainline/xmldsign

Что еще полезно знать при работе с КриптоПро

Если вам необходимо подписывать данные ключом, выданным в ФНС, имейте в виду, его можно законно использовать, только воткнув флэшку с ключом в компьютер, на котором подписываются документы. Экспортирование такого ключа запрещено и это нужно учитывать при построении архитектуры вашего сервиса подписи.

Это же обстоятельство затрудняет разработку в докере с использованием такого ключа. Может быть кто-то в комментариях поделится рабочим алгоритмом проброса рутокена в докер, но мне на макбуке за разумное время настроить это не удалось.

Полезные ссылки
  1. Сервис для проверки электронной подписи https://dss.cryptopro.ru/verify/#/signature

  2. Сервис для получения тестового ключа http://testca2012.cryptopro.ru/ui/

  3. Документация по сборке и установке расширения КриптоПро ​​https://docs.cryptopro.ru/cades/phpcades/phpcades-install

  4. Исходники расширения  https://github.com/CryptoPro/phpcades/

  5. Реализация Smev-трансформ от Danbka  https://github.com/Danbka/smev-transform

  6. Проект, документирующий возможности КриптоПро и включающий автоподсказки в PhpStorm  (незаконченный) https://github.com/crypto-pro-web/crypto-pro-php-stubs

  7. Стандарт XMLDSig https://www.w3.org/TR/xmldsig-core1/

Комментарии (0)