Отправка сообщения в ДМДК через stunnel на С#

от автора

Задача — у нас есть розничные продажи и нам надо отправлять информацию о них в госсистему ДМДК.

Как зарегистрироваться в ДМДК и настроить stunnel я напишу отдельную статью, считаем что он есть, настроен и работает. Соответственно у нас есть ЭЦП, все необходимые сертификаты зарегистрированы.

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

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

Образец шаблона нашего сообщения о продаже:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="urn://xsd.dmdk.goznak.ru/exchange/3.0" xmlns:ns1="urn://xsd.dmdk.goznak.ru/saleoperation/3.0"> <soapenv:Header/>  <soapenv:Body> <ns:SendBatchSaleRequest> <ns:CallerSignature> </ns:CallerSignature> <ns:RequestData id="data"> <ns:sale> <ns1:index>1</ns1:index> <ns1:type>SALE</ns1:type> <ns1:cheque> <ns1:fn></ns1:fn> <ns1:fd>CASH_RECEIPT</ns1:fd> <ns1:nfd>000</ns1:nfd> <ns1:date>YYYY-MM-DD</ns1:date> </ns1:cheque> </ns:sale> </ns:RequestData> </ns:SendBatchSaleRequest> </soapenv:Body> </soapenv:Envelope>

Теперь можно сформировать сообщение для отправки на основе наших данных:

public void SendDmdk() {     var xdoc = new XmlDocument()     {         PreserveWhitespace = false     };     xdoc.Load("dmdk.xml");     var nfd = xdoc.GetElementsByTagName("ns1:nfd")[0];     nfd.InnerText = bace.receipt.fn.ToString();     xdoc.GetElementsByTagName("ns1:date")[0].InnerText = bace.receipt.date.ToString("yyyy-MM-dd");     var cheque = xdoc.GetElementsByTagName("ns1:cheque")[0];      //Выбираем уины из чека     var uin = (from p in bace.docTable                where !string.IsNullOrWhiteSpace(p.mark)                select p.mark).ToList();     foreach (var m in uin)     {         XmlElement uinList = xdoc.CreateElement("ns1","uinList",cheque.NamespaceURI);         XmlElement xUin = xdoc.CreateElement("ns1","UIN",cheque.NamespaceURI);         xUin.InnerText = m.Trim();                  uinList.AppendChild(xUin);         cheque.AppendChild(uinList);         Systems.Dmdk.SetPrefix("ns1", cheque);     }     //Отправляем сообщение     string result = Systems.Dmdk.SendXml(xdoc);      //Помечаем что отправили, если не получилось - оно вываливает эксепшн     bace.receipt.dkdm = 1;     Save(); }

Сообщение сформировали, теперь его надо подписать нашей ЭЦП, добавив подпись в наш XML. Для этого воспользуемся СКЗИ Криптопро с его библиотекой Cryptopro.NET

public static string SendXml(XmlDocument xdoc) {     //Подпишем документ перед отправкой     X509Certificate2 certificate = GetX509Certificate();     var key = certificate.PrivateKey;      // Создаем объект SignedXml по XML документу.     var signedXml = new PrefixedSignedXml(xdoc, "ds");     signedXml.SigningKey = key;     // Создаем ссылку на node для подписи.     Reference dataReference = new Reference();     dataReference.Uri = "#data";      // Явно проставляем алгоритм хэширования,     // по умолчанию SHA1.     dataReference.DigestMethod = CPSignedXml.XmlDsigGost3411_2012_256Url;     dataReference.AddTransform(new XmlDsigExcC14NTransform());     dataReference.AddTransform(new XmlDsigSmevTransform());      signedXml.SafeCanonicalizationMethods.Add("urn://smev-gov-ru/xmldsig/transform");      // Установка ссылки на узел     signedXml.AddReference(dataReference);       // Создаем объект KeyInfo.     KeyInfo keyInfo = new KeyInfo();      // Добавляем сертификат в KeyInfo     keyInfo.AddClause(new KeyInfoX509Data(certificate));      // Добавляем KeyInfo в SignedXml.     signedXml.KeyInfo = keyInfo;      // Можно явно проставить алгоритм подписи: ГОСТ Р 34.10.     // Если сертификат ключа подписи ГОСТ Р 34.10     // и алгоритм ключа подписи не задан, то будет использован     //XmlDsigGost3410Url     signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;     signedXml.SignedInfo.SignatureMethod = CPSignedXml.XmlDsigGost3410_2012_256Url;      // Вычисляем подпись.     signedXml.ComputeSignature();      // Получаем XML представление подписи и сохраняем его      // в отдельном node.     XmlElement xmlDigitalSignature = signedXml.GetXml();      xdoc.GetElementsByTagName("ns:CallerSignature")[0].AppendChild(xdoc.ImportNode(xmlDigitalSignature, true));      string url = "http://127.0.0.1:1501/ws/v3";     System.Net.HttpWebRequest reqPOST = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url);     reqPOST.Method = "POST";     reqPOST.ContentType = "text/xml; charset=UTF8";     reqPOST.Timeout = 120000;     reqPOST.Accept = "text/xml";     XmlWriter xmlWriter = new XmlTextWriter(reqPOST.GetRequestStream(),                               System.Text.Encoding.UTF8);     xdoc.WriteTo(xmlWriter);     xmlWriter.Close();     var response = reqPOST.GetResponse();     string t = "";     using (StreamReader sr = new StreamReader(response.GetResponseStream()))     {         var responseString = sr.ReadToEnd();         t = responseString;     }     response.Close();     return t; }

Тут используем выбор сертификата:

public static X509Certificate2 GetX509Certificate() {     // Формуруем коллекцию отображаемых сертификатов.     X509Store store = new X509Store("MY", StoreLocation.CurrentUser);     store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);     X509Certificate2Collection collection =         (X509Certificate2Collection)store.Certificates;      //Выбираем нужный сертификат из коллекции     foreach (var i in collection)     {         //Выбираем сертификат по шаблону имени, чтобы не прелдагать пользователю выбирать сертифкат каждый раз         if (i.Subject.ToUpper().Contains(sertShablonValue)) return i;     }     // Отображаем окно выбора сертификата.     X509Certificate2Collection scollection =         X509Certificate2UI.SelectFromCollection(collection,         "Выбор секретного ключа по сертификату",         "Выберите сертификат соответствующий Вашему секретному ключу.",         X509SelectionFlag.MultiSelection);      // Проверяем, что выбран сертификат     if (scollection.Count == 0)     {         throw new Exception("Не выбран ни один сертификат.");     }     // Выбран может быть только один сертификат.     return scollection[0]; }

Включаем — не работает 🙂

Оказывается надо обязательно указывать префиксы иначе ДМДК некорректно разбирает наш XML. В коде выше уже добавлен исправленный класс PrefixedSignedXml, вместо используемого по умолчанию SignedXml, в него сразу встроим вычисление подписи, добавление необходимых префиксов и окончательную упаковку отправляемого файла.

public class PrefixedSignedXml : SignedXml {     private readonly string _prefix;      public PrefixedSignedXml(XmlDocument document, string prefix)         : base(document)     {         _prefix = prefix;     }      public new void ComputeSignature()     {         BuildDigestedReferences();         var signingKey = SigningKey;         if (signingKey == null)         {             throw new CryptographicException("Cryptography_Xml_LoadKeyFailed");         }         if (SignedInfo.SignatureMethod == null)         {             if (!(signingKey is DSA))             {                 if (!(signingKey is RSA))                 {                     throw new CryptographicException("Cryptography_Xml_CreatedKeyFailed");                 }                  SignedInfo.SignatureMethod = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";             }             else             {                 SignedInfo.SignatureMethod = "http://www.w3.org/2000/09/xmldsig#dsa-sha1";             }         }          if (!(CryptoConfig.CreateFromName(SignedInfo.SignatureMethod) is SignatureDescription description))         {             throw new CryptographicException("Cryptography_Xml_SignatureDescriptionNotCreated");         }         var hash = description.CreateDigest();         if (hash == null)         {             throw new CryptographicException("Cryptography_Xml_CreateHashAlgorithmFailed");         }         GetC14NDigest(hash, _prefix);         m_signature.SignatureValue = description.CreateFormatter(signingKey).CreateSignature(hash);     }      public new XmlElement GetXml()     {         var e = base.GetXml();         SetPrefix(_prefix, e);         return e;     }      //Отражательно вызывать закрытый метод SignedXml.BuildDigestedReferences     private void BuildDigestedReferences()     {         var t = typeof(SignedXml);         var m = t.GetMethod("BuildDigestedReferences", BindingFlags.NonPublic | BindingFlags.Instance);         m?.Invoke(this, new object[] { });     }      private void GetC14NDigest(HashAlgorithm hash, string prefix)     {         var document = new XmlDocument         {             PreserveWhitespace = true         };         var e = SignedInfo.GetXml();         document.AppendChild(document.ImportNode(e, true));         var canonicalizationMethodObject = SignedInfo.CanonicalizationMethodObject;         SetPrefix(prefix, document.DocumentElement); //мы устанавливаем префикс перед вычислением хеша (иначе подпись не будет действительной)         canonicalizationMethodObject.LoadInput(document);         canonicalizationMethodObject.GetDigestedOutput(hash);     } }

Отправляем, проверяем в личном кабинете — все работает. Надеюсь этот опус поможет сэкономить день на ходьбу по граблям данной системы.


ссылка на оригинал статьи https://habr.com/ru/articles/841738/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *