Sending a message to DMDK via stunnel in C#

The task is that we have retail sales and we need to send information about them to state system of DMDK.

How to register in DMDK and configure stunnel I will write a separate article, we assume that it exists, is configured and works. Accordingly, we have an EDS, all the necessary certificates are registered.

Next, we go into the service documentation and take the structure of the sent xml file as a template; for simplicity of solution, it was decided to save this template as a file in the program folder and put the necessary data there.

We decide to transmit each receipt as a separate message, fortunately there are not many of them per day and it is possible to transmit not immediately, but within a few days after the sale. We will transmit in the background asynchronously, and try to transmit until the DMDK eats it (sometimes it glitches, sometimes it does not work, sometimes it is on maintenance).

Sample template of our sales message:

<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>

Now we can form a message to send based on our data:

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();
}

The message has been generated, now it needs to be signed with our digital signature by adding the signature to our XML. To do this, we will use the Cryptopro cryptographic information protection tool with its Cryptopro.NET library.

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;
}

Here we use the certificate selection:

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];
}

We turn it on – it doesn't work 🙂

It turns out that it is necessary to specify prefixes, otherwise DMDK will not parse our XML correctly. The code above already has a corrected PrefixedSignedXml class added instead of the default SignedXml, we will immediately build in the signature calculation, adding the necessary prefixes and the final packaging of the file being sent.

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);
    }
}

We send it, check it in the personal account – everything works. I hope this opus will help save a day of walking on the rake of this system.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *