ДВС ЖКГ: встановлення захищеного з'єднання і підписання повідомлень з допомогою WCF

Зараз активно допрацьовується державна інформаційна система ЖКГ, а з 1 січня 2017 року настає відповідальність для керівників і ресурсопостачаючих організацій за неподання інформації у системі.

Як показує практика, розробники, які виконують інтеграцію своїх інформаційних систем з ДВС ЖКГ, дуже багато часу витрачають на встановлення захищеного з'єднання і підписання повідомлень. Незважаючи на те, що розробниками ДВС ЖКГ надано демонстраційне додаток підписання повідомлень, я опишу як можна розв'язати цю задачу за допомогою WCF. Хочу зазначити, що в цьому випадку додаткове ПЗ (типу stunnel) для встановлення захищеного з'єднання не потрібно.

Сподіваюся, ця стаття допоможе розробникам, які в майбутньому будуть інтегруватися з ДВС ЖКГ.

На жаль, без покупки додаткового програмного забезпечення не обійтися. Нам потрібен КриптоПро .NET. Є тримісячний безкоштовний термін використання. Ця бібліотека буде забезпечувати https з'єднання і підпис повідомлень за алгоритмом XAdES-BES. Також не забуваємо, що потрібен сертифікат кваліфікованої електронного підпису.

Підготовка
Для початку потрібно встановити сертифікат кваліфікованої електронного підпису в особисте сховище локального комп'ютера. Він повинен бути встановлений разом з закритим ключем.
Детально описувати процес тут не буду, інформацію можна знайти на тут і тут.

Генерація проксі-класів
На сайті ДВС ЖКГ у розділі Регламенти та інструкції файл «Регламент та формати інформаційного взаємодії зовнішніх інформаційних систем з ДВС ЖКГ». Поточна версія 10.0.1.2. У цьому файлі знаходяться wsdl і xsd файли, ми їх будемо використовувати для створення проксі-класів WCF.

Скопіюємо всі wsdl і xsd файли в яку-небудь папку, наприклад «c:/gis». Це потрібно для того, щоб утиліта генерації могла знайти всі базові xsd файли, на які використовує xsd файл сервісу.

Для генерації проксі-класів ми будемо використовувати стандартну утиліту SvcUtil.

"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\SvcUtil.exe" c:/gis/hcs-nsi-common-service.wsdl c:/gis/*.xsd /messageContract /enableDataBinding /syncOnly /directory:"c:/gis/proxies" /noConfig /noLogo /out:NsiCommonService.cs /namespace:*,Gis.Infrastructure.NsiCommonService

Ця команда створить нам проксі-клас сервісу для отримання загальних довідників (hcs-nsi-common). У цій команді вказано, де знайти wsdl і xsd файл опису сервісу, куди покласти результуючий cs файл і як назвати namespace. Аналогічно потрібно запустити цю команду для інших сервісів ДВС ЖКГ.

Додамо згенерований проксі-клас у проект

Налаштування конфига програми
Додамо в конфіг додатки наступні розділи

<configuration> 
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="MessageInspectorBehavior" type="Gis.Crypto.MessageInspectorBehaviorElement, Gis" />
</behaviorExtensions>
</extensions>
<behaviors>
<endpointBehaviors>
<behavior name="clientCertificateConf">
<clientCredentials>
<!-- у блоці findValue вказується серійний ключ сертифіката -->
<clientCertificate findValue="" storeLocation="LocalMachine" x509FindType="FindBySerialNumber" />
<serviceCertificate>
<authentication certificateValidationMode="Ні" revocationMode="дозволяє nochex" />
</serviceCertificate>
</clientCredentials>
<MessageInspectorBehavior />
</behavior>
</endpointBehaviors>
</behaviors>
<bindings>
<customBinding>
<binding>
<textMessageEncoding messageVersion="Soap11">
<readerQuotas maxDepth="32" maxStringContentLength="2147483647" maxArrayLength="16348" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
</textMessageEncoding>
<httpsTransport authenticationScheme="Basic" useDefaultWebProxy="false" requireClientCertificate="true" maxBufferSize="2147483647" maxReceivedMessageSize="2147483647" />
</binding>
</customBinding>
</bindings>
<client>
<endpoint address="https://api.dom.gosuslugi.ru/ext-bus-nsi-common-service/services/NsiCommon" binding="customBinding" behaviorConfiguration="clientCertificateConf"
contract="Gis.Infrastructure.NsiCommonService.NsiPortsType" name="NsiCommonPort" />
</client>
</system.serviceModel>
</configuration>

Опис класів
В конфіги ми реєструємо MessageInspectorBehavior, який додає ClientMessageInspector:

public class MessageInspectorBehavior : IEndpointBehavior
{
public void Validate(ServiceEndpoint endpoint)
{
}

public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
}

public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
SignatureMessageInspector inspector =
new SignatureMessageInspector();
clientRuntime.MessageInspectors.Add(inspector);
}
}

Ще наведу лістинг класу SignatureMessageInspector, який і займається підписанням повідомлень:

public class SignatureMessageInspector : IClientMessageInspector
{
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
string st = GetSignElement(MessageString(ref request));
//place for log request
request = CreateMessageFromString(st, request.Version);
return null;
}

public void AfterReceiveReply(ref Message reply, object correlationState)
{
string st = MessageString(ref reply);
//place for log response
reply = CreateMessageFromString(st, reply.Version);
}

public static string GetSignElement(string messageString)
{
var originalDoc = new XmlDocument { PreserveWhitespace = true };
originalDoc.LoadXml(messageString);

var nodes = originalDoc.SelectNodes($"//node()[@Id='{CryptoConsts.CONTAINER_ID}']");
if (nodes == null || nodes.Count == 0)
{
return originalDoc.OuterXml;
}

var gostXadesBesService = new GostXadesBesService();

string st = gostXadesBesService.Sign(messageString, CryptoConsts.CONTAINER_ID, CryptoConsts.CERTIFICATE_THUMBPRINT, string.Empty);

return st;
}

Message CreateMessageFromString(String xml, MessageVersion ver)
{
return Message.CreateMessage(XmlReaderFromString(xml), int.MaxValue, ver);
}

XmlReader XmlReaderFromString(String xml)
{
var stream = new MemoryStream();
// NOTE: don't use using(var writer ...){...}
// because the end of the StreamWriter's using closes the Stream itself.
//
var writer = new StreamWriter(stream);
writer.Write(xml);
writer.Flush();
stream.Position = 0;
return XmlReader.Create(stream);
}

String MessageString(ref Message m)
{
// copy the message into a working buffer.
MessageBuffer mb = m.CreateBufferedCopy(int.MaxValue);

// re-create the original message, because "copy" changes its state.
m = mb.CreateMessage();

Stream s = new MemoryStream();
XmlWriter xw = XmlWriter.Create(s);
mb.CreateMessage().WriteMessage(xw);
xw.Flush();
s.Position = 0;

byte[] bXml = new byte[s.Length];
s.Read(bXml, 0, (int) s.Length);

// sometimes bXML[] starts with a BOM
if (bXml[0] != (byte) '<')
{
return Encoding.UTF8.GetString(bXml, 3, bXml.Length - 3);
}
return Encoding.UTF8.GetString(bXml, 0, bXml.Length);
}
}

Використання
Для безпосереднього виконання запиту до ДВС ЖКГ необхідно створити екземпляр клієнта згенерованого проксі-класу, вказати налаштування клієнтської авторизації, сформувати об'єкт запиту та викликати необхідний метод:

class Program
{
static void Main(string[] args)
{
var service = new NsiPortsTypeClient();
service.ClientCredentials.UserName.UserName = "lanit";
service.ClientCredentials.UserName.Password = "tv,n8!Ja";

var request = new exportNsiListRequest1
{
ISRequestHeader = new HeaderType
{
Date = DateTime.Now,
MessageGUID = Guid.NewGuid().ToString()
},
exportNsiListRequest = new exportNsiListRequest
{
version = "10.0.1.2",
ListGroup = ListGroup.NSI,
ListGroupSpecified = true,
Id = CryptoConsts.CONTAINER_ID
}
};

var result = service.exportNsiList(request);
}
}

» Вихідний код проекту
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.