Задача — у нас есть розничные продажи и нам надо отправлять информацию о них в госсистему ДМДК.
Как зарегистрироваться в ДМДК и настроить 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/
Добавить комментарий