ГИС ЖКХ — отправка и подпись запросов часть 3
В продолжение предыдущей статьи. На основе того кода, так и не удалось довести до рабочего состояния. Ошибка так и осталась. Посему было решено..опять всё переписать с нуля.. С учётом полученных новых вводных, а именно:
- XML подписи оказывается должен быть минимизирован (не должно быть форматирования тегов)
- подписываться должен хэш данных, а не сами данные
- при работе с XML как текстом, были опасения про неверную каннизацию
Потому переписал код на отностительно честную работу с XML через DOMDocument. С одним нюансом — так и не научился вставлять один XML документ в другой посредством его. Поэтому присутствует трюк с переводом XML в текст, склейкой двух XML и обратный перевод в DOMDocument. XML шаблоны удалось сократить до двух:
fish.xml:
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:base="http://dom.gosuslugi.ru/schema/integration/base/" xmlns:drs="http://dom.gosuslugi.ru/schema/integration/drs/" xmlns:xd="http://www.w3.org/2000/09/xmldsig#"> <soapenv:Header> <base:RequestHeader> <base:Date>2025-03-19T11:26:00+03:00</base:Date> <base:MessageGUID>${=java.util.UUID.randomUUID()}</base:MessageGUID> <base:orgPPAGUID>c8913980-8cbb-4a46-bdac-7fecd98c7ff8</base:orgPPAGUID> </base:RequestHeader> </soapenv:Header> <soapenv:Body xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><drs:exportDSRsRequest Id="{signed_id}" base:version="15.3.0.0"><drs:periodOfSendingRequest><base:startDate>2025-05-27</base:startDate><base:endDate>2025-06-28</base:endDate></drs:periodOfSendingRequest></drs:exportDSRsRequest></soapenv:Body></soapenv:Envelope> |
xades_dom.xml:
1 |
<ds:Signature Id="xmldsig-{signature_id}" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><ds:SignatureMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102012-gostr34112012-256"/><ds:Reference Id="xmldsig-{signature_id}-ref0" URI="#{signed_id}"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256"/><ds:DigestValue>{digest1}</ds:DigestValue></ds:Reference><ds:Reference Type="http://uri.etsi.org/01903#SignedProperties" URI="#xmldsig-{signature_id}-signedprops"><ds:DigestMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256"/><ds:DigestValue>{digest3}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue Id="xmldsig-{signature_id}-sigvalue">{signature_value}</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>{x590_cert}</ds:X509Certificate></ds:X509Data></ds:KeyInfo><ds:Object><xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#xmldsig-{signature_id}"><xades:SignedProperties Id="xmldsig-{signature_id}-signedprops"><xades:SignedSignatureProperties><xades:SigningTime>{signing_time}</xades:SigningTime><xades:SigningCertificate><xades:Cert><xades:CertDigest><DigestMethod xmlns="http://www.w3.org/2000/09/xmldsig#" Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256"/><DigestValue xmlns="http://www.w3.org/2000/09/xmldsig#">{digest2}</DigestValue></xades:CertDigest><xades:IssuerSerial><X509IssuerName xmlns="http://www.w3.org/2000/09/xmldsig#">{x509_issuer_name}</X509IssuerName><X509SerialNumber xmlns="http://www.w3.org/2000/09/xmldsig#">{x509_sn}</X509SerialNumber></xades:IssuerSerial></xades:Cert></xades:SigningCertificate></xades:SignedSignatureProperties></xades:SignedProperties></xades:QualifyingProperties></ds:Object></ds:Signature> |
И сам код скрипта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
<?php $fish_file='in/fish.xml'; // xml - главный шаблон, откуда осуществляем сборку $xades_file='in/xades_dom.xml'; // xml - шаблон подписи $cert_file='keys/cert.key'; // Сертификат формата X590 $private_file='keys/priv.key'; // Закрытый ключ function md_gost($text){ file_put_contents("tmp.xml", $text); $res=`cat tmp.xml | openssl dgst -binary -md_gost12_256 | base64 -w 0`; $res= str_replace("\n", "", $res); return trim($res); }; function md_gost_bin($text){ file_put_contents("tmp.xml", $text); $res=`cat tmp.xml | openssl dgst -binary -md_gost12_256>hash.bin`; return file_get_contents("hash.bin"); }; function get_issuer($cert_file){ $res=`openssl x509 -in $cert_file -issuer -nameopt sep_multiline,utf8 -noout`; $res= str_replace("issuer=\n", "", $res); $res= str_replace("emailaddress", "1.2.840.113549.1.9.1", $res); $res= str_replace("inn", "1.2.643.100.4", $res); $res= str_replace("ogrn", "1.2.643.100.1", $res); $res= str_replace('"', "\\", $res); $res= str_replace(',', "\,", $res); $res= explode("\n", $res); $result=""; for ($index = count($res)-1; $index > 0; $index--) { if (trim($res[$index])!=""){ $prop= explode("=",$res[$index]); $result=$result.strtolower(trim($prop[0]))."=".trim($prop[1]); if ($index>1){ $result=$result.","; }; }; }; return trim($result); } function get_serial($cert_file){ $res=`openssl x509 -in $cert_file -serial -nameopt sep_multiline,utf8 -noout`; $res= str_replace("serial=", "", $res); $res= str_replace("\n", "", $res); return base_convert(trim($res),16,10); } function get_signature($text, $private_file){ file_put_contents("for_sign.xml", $text); $res=`cat for_sign.xml | openssl dgst -md_gost12_256 -sign $private_file -binary | base64 -w 0`; $res= str_replace("\n", "", $res); return trim($res); } function X509($cert_file){ $crt= file_get_contents($cert_file); $crt= str_replace("-----BEGIN CERTIFICATE-----", "", $crt); $crt= str_replace("-----END CERTIFICATE-----", "", $crt); $crt= str_replace("\n", "", $crt); $crt= str_replace("\r", "", $crt); return trim($crt); } function guidv4($data = null) { $data = $data ?? random_bytes(16); assert(strlen($data) == 16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); $data[8] = chr(ord($data[8]) & 0x3f | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } $dt=new DateTime(); $signing_time=$dt->format(DateTime::ATOM); $signature_id=guidv4(); // идентификатор блока "подпись" $signed_id=guidv4(); // идентификатор блока который подписываем $fish_xml=file_get_contents($fish_file); $dom = new DOMDocument('1.0', 'utf-8'); $dom->loadXML($fish_xml); $root = $dom->documentElement; $req_xml=$root->getElementsByTagName('exportDSRsRequest')->item(0); $req_xml->setAttribute('Id',$signed_id); $req_can=$req_xml->C14N(true); $digest1=md_gost($req_can); echo "req=$req_can\n"; echo "digest1=$digest1\n"; $digest2= md_gost(base64_decode(X509($cert_file))); echo "digest2=$digest2\n"; $dom = new DOMDocument('1.0', 'utf-8'); $tmp_text_xml=$root->C14N(true); // подмешиваем подпись $xades= file_get_contents($xades_file); $tmp_text_xml= str_replace('base:version="15.3.0.0">', 'base:version="15.3.0.0">'.$xades, $tmp_text_xml); $tmp_text_xml= str_replace("{signing_time}", $signing_time, $tmp_text_xml); $tmp_text_xml= str_replace("{signature_id}", $signature_id, $tmp_text_xml); $tmp_text_xml= str_replace("{digest1}", $digest1, $tmp_text_xml); $tmp_text_xml= str_replace("{digest2}", $digest2, $tmp_text_xml); $tmp_text_xml= str_replace("{x509_issuer_name}", get_issuer($cert_file), $tmp_text_xml); $tmp_text_xml= str_replace("{x509_sn}", get_serial($cert_file), $tmp_text_xml); $dom->loadXML($tmp_text_xml); $root = $dom->documentElement; $digest3=md_gost($root->getElementsByTagName('SignedProperties')->item(0)->C14N(true)); echo "digest3=$digest3\n"; $dom = new DOMDocument('1.0', 'utf-8'); $tmp_text_xml=$root->C14N(true); $tmp_text_xml= str_replace("{digest3}", $digest3, $tmp_text_xml); $tmp_text_xml= str_replace("{x590_cert}", X509($cert_file), $tmp_text_xml); $tmp_text_xml= str_replace("{signed_id}", $signed_id, $tmp_text_xml); $dom->loadXML($tmp_text_xml); $root = $dom->documentElement; // подписываем $for_sign=$root->getElementsByTagName('SignedInfo')->item(0)->C14N(true); $hash_for_sign= md_gost_bin($for_sign); $sign=get_signature($hash_for_sign,$private_file); echo "sign=$sign\n"; $dom = new DOMDocument('1.0', 'utf-8'); $tmp_text_xml=$root->C14N(true); $tmp_text_xml= str_replace("{signature_value}", $sign, $tmp_text_xml); $dom->loadXML($tmp_text_xml); $root = $dom->documentElement; // echo "---------------- ИТОГО ---------------\n"; file_put_contents("out.xml", $root->C14N(true)); |
Но увы и ах.. Результат его работы хоть с виду еще более нормальный, но ГИС ЖКХ так и продолжает возвращать ошибку:
1 2 |
<ns4:ErrorCode>AUT011005</ns4:ErrorCode> <ns4:Description>Ошибка формата подписи запроса</ns4:Description> |