ГИС ЖКХ. Отправка и подпись сообщений на PHP
Ранее была статья, с реализацией подписи на Python. Переписал всё то-же самое на PHP. Ну на тот случай, если вдруг какие-то нюансы упустил в реализации. Однако нет, ошибка та-же:
1 2 |
<ns4:ErrorCode>AUT011005</ns4:ErrorCode> <ns4:Description>Ошибка формата подписи запроса</ns4:Description> |
Ниже описываю как формирую запрос. Он собирается из нескольких файлов xml:
in.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?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>cerfwerfewrfer8</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"> {req} </soapenv:Body> </soapenv:Envelope> |
req.xml — непосредственно запрос к ГИС ЖКХ. В данном случае получение списка запросов от сторонних организаций о задолженностях потребителей
1 2 3 |
<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> |
xades.xml:
1 2 3 4 5 6 7 8 9 10 11 12 |
<ds:Signature Id="xmldsig-{signature_id}" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> {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}">{SignedProperties}</xades:QualifyingProperties> </ds:Object> </ds:Signature> |
sp.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<xades:SignedProperties Id="xmldsig-{signature_id}-signedprops"> <xades:SignedSignatureProperties> <xades:SigningTime>{signing_time}</xades:SigningTime> <xades:SigningCertificate> <xades:Cert> <xades:CertDigest> <ds:DigestMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256"/> <ds:DigestValue>{digest2}</ds:DigestValue> </xades:CertDigest> <xades:IssuerSerial> <ds:X509IssuerName>{x509_issuer_name}</ds:X509IssuerName> <ds:X509SerialNumber>{x509_sn}</ds:X509SerialNumber> </xades:IssuerSerial> </xades:Cert> </xades:SigningCertificate> </xades:SignedSignatureProperties> </xades:SignedProperties> |
si.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<ds:SignedInfo> <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> <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> |
Т.е. общий алгоритм следующий — заполняем в каждом файлике значения в фигурных скобках, а затем склеиваем всё в единый файл out.xml, который уже непосредственно отправляем по SOAP через туннель с ГИС ЖКХ.
Для начала понадобятся следующие функции:
Вычисление хэш строки по алгоритму ГОСТ Р 34.11-2012 256 бит:
1 2 3 4 5 6 |
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); }; |
Получается строка вида: JSO1ALXjmpWXGkdQAs4RcEroYUnXOdbp5j4tbG8bNN8= в формате base64
Получение эмитента сертификата:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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); } |
Особенностью является «перевертывание» информации, нормализация отображения, а так-же замена ИНН, ОГРН и email на цифровые коды. Согласно рекомендации из «Альбом ТФФ». У меня получилась строка вида:
1 |
cn=Федеральная налоговая служба,o=Федеральная налоговая служба,street=ул. Неглинная\, д. 23,l=г. Москва,st=77 Москва,c=RU,ogrn=1047707030513,emailaddress=uc@tax.gov.ru |
Получение серийного номера сертификата:
1 2 3 4 5 6 |
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); } |
Согласно Альбому ТФФ, он должен быть в десятичном формате. Вида:
1 |
778141053723503123230066408008688602 |
Канонинализация XML:
Для того чтобы в XML не было лишних и не значащих символов, данные перед подписанием и вычислением хэша необходимо канонинализировать:
1 2 3 4 5 6 |
function CanonicalXML($xml_text){ $xml = new DOMDocument( "1.0", "utf-8" ); @$xml->loadXML($xml_text); $xml = @$xml->C14N(); return $xml; } |
Получение сертификата в формате Base64:
1 2 3 4 5 6 7 8 |
function X590($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); } |
Получение подписи текста:
1 2 3 4 5 6 |
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); } |
Возвращается в формате base64
Получение UUID:
1 2 3 4 5 6 7 |
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)); } |
Далее идет уже непосредственно уже вычисление необходимых блоков и формирование выходного файла:
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 |
$in_file='in/in.xml'; // xml - главный шаблон, откуда осуществляем сборку $req_file='in/req.xml'; // запрос в ГИС ЖКХ $xades_file='in/xades.xml'; // Рыба подписи $sp_file='in/sp.xml'; // Свойства подписи $si_file='in/si.xml'; // Непосредственно подписываемый блок $cert_file='keys/cert.key'; // Сертификат формата X590 $private_file='keys/priv.key'; // Закрытый ключ $signed_id=guidv4(); $in_xml=file_get_contents($in_file); $req_xml=CanonicalXML(file_get_contents($req_file)); $req_xml= str_replace("{signed_id}", $signed_id, $req_xml); $xades_xml=file_get_contents($xades_file); $digest1=md_gost($req_xml); echo "digest1=$digest1\n"; $digest2= md_gost(base64_decode(X590($cert_file))); echo "digest2=$digest2\n"; $dt=new DateTime(); $signing_time=$dt->format(DateTime::ATOM); $signature_id=guidv4(); $sp_xml=CanonicalXML(file_get_contents($sp_file)); $sp_xml= str_replace("{signing_time}", $signing_time, $sp_xml); $sp_xml= str_replace("{digest2}", $digest2, $sp_xml); $sp_xml= str_replace("{x509_issuer_name}", get_issuer($cert_file), $sp_xml); $sp_xml= str_replace("{x509_sn}", get_serial($cert_file), $sp_xml); $digest3=md_gost($sp_xml); echo "digest3=$digest3\n"; $si_xml=CanonicalXML(file_get_contents($si_file)); $si_xml= str_replace("{digest3}", $digest3, $si_xml); $si_xml= str_replace("{digest1}", $digest1, $si_xml); $si_xml= str_replace("{signature_id}", $signature_id, $si_xml); $si_xml= str_replace("{signed_id}", $signed_id, $si_xml); $signature_value=get_signature($si_xml,$private_file); echo "signature_value=$signature_value\n"; $xades_xml= str_replace("{signature_value}", $signature_value, $xades_xml); $xades_xml= str_replace("{x590_cert}", X590($cert_file), $xades_xml); $xades_xml= str_replace("{SignedProperties}",$sp_xml, $xades_xml); $xades_xml= str_replace("{ds:SignedInfo}",$si_xml, $xades_xml); $xades_xml= str_replace("{signature_id}", $signature_id, $xades_xml); //$in_xml= str_replace("{xades}",$xades_xml, $in_xml); var_dump($req_xml); $pz= strpos($req_xml, ">"); $req_1= trim(substr($req_xml,0,$pz+1)); $req_2= trim(substr($req_xml,$pz+1, strlen($req_xml)-$pz)); $req_xml=$req_1.$xades_xml.$req_2; $in_xml= str_replace("{req}",$req_xml, $in_xml); file_put_contents("out/out.xml",$in_xml); |
Далее полученный файлик можно вставить например в SOAP UI, и попробовать отправить.
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 |
<?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>укепуепукепукеп</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="938803c1-341c-41c5-8d24-10fa15dde654" base:version="15.3.0.0"><ds:Signature Id="xmldsig-51a19c5a-7c25-432c-9340-dd7582d4d3ef" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:SignedInfo> <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod> <ds:SignatureMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102012-gostr34112012-256"></ds:SignatureMethod> <ds:Reference Id="xmldsig-51a19c5a-7c25-432c-9340-dd7582d4d3ef-ref0" URI="#938803c1-341c-41c5-8d24-10fa15dde654"> <ds:Transforms> <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform> <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform> </ds:Transforms> <ds:DigestMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256"></ds:DigestMethod> <ds:DigestValue>324к234к234к234к/6dgGHM4k52/RDm855aKazGXEeK4=</ds:DigestValue> </ds:Reference> <ds:Reference Type="http://uri.etsi.org/01903#SignedProperties" URI="#xmldsig-51a19c5a-7c25-432c-9340-dd7582d4d3ef-signedprops"> <ds:DigestMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256"></ds:DigestMethod> <ds:DigestValue>34к34к234к234к3</ds:DigestValue> </ds:Reference> </ds:SignedInfo> <ds:SignatureValue Id="xmldsig-51a19c5a-7c25-432c-9340-dd7582d4d3ef-sigvalue">mWUI++9N1uEe/234к234к/укеаукепуке</ds:SignatureValue> <ds:KeyInfo> <ds:X509Data> <ds:X509Certificate>укепукепкуепк/укепукепуке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</ds:X509Certificate> </ds:X509Data> </ds:KeyInfo> <ds:Object> <xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#xmldsig-51a19c5a-7c25-432c-9340-dd7582d4d3ef"><xades:SignedProperties Id="xmldsig-51a19c5a-7c25-432c-9340-dd7582d4d3ef-signedprops"> <xades:SignedSignatureProperties> <xades:SigningTime>2025-06-17T10:31:13+03:00</xades:SigningTime> <xades:SigningCertificate> <xades:Cert> <xades:CertDigest> <ds:DigestMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256"></ds:DigestMethod> <ds:DigestValue>кепукепук</ds:DigestValue> </xades:CertDigest> <xades:IssuerSerial> <ds:X509IssuerName>cn=Федеральная налоговая служба,o=Федеральная налоговая служба,street=ул. Неглинная\, д. 23,l=г. Москва,st=77 Москва,c=RU,ogrn=1047707030513,emailaddress=uc@tax.gov.ru</ds:X509IssuerName> <ds:X509SerialNumber>45е34е5354е43е</ds:X509SerialNumber> </xades:IssuerSerial> </xades:Cert> </xades:SigningCertificate> </xades:SignedSignatureProperties> </xades:SignedProperties></xades:QualifyingProperties> </ds:Object> </ds:Signature><drs:periodOfSendingRequest><base:startDate>2025-05-27</base:startDate><base:endDate>2025-06-28</base:endDate></drs:periodOfSendingRequest> </drs:exportDSRsRequest> </soapenv:Body> </soapenv:Envelope> |
В ответ получим номер идентификатора запроса. Далее его используем в запросе getState.. И получаем ошибку которая в самом начале статьи. Что не так — пока не понятно.