Подпись сообщений SOAP для СМЭВ с использованием КриптоПро .NET

В соответствии с требованиями, обозначенными в приказе Министерства связи и массовых коммуникаций Российской Федерации от 27.12.2010 №190 от «Об утверждении Технических требований к взаимодействию информационных систем в единой системе межведомственного электронного взаимодействия» (полный текст приказа доступен здесь) и Методическими рекомендациями по разработке электронных сервисов и применению технологии электронной подписи при межведомственном электронном взаимодействии (скачать документ можно здесь), при интеграции информационных систем в систему межведомственного электронного взаимодействия (далее СМЭВ) необходимо среди прочего придерживаться спецификаций на протокол обмена структурированными сообщениями (Simple Object Access Protocol, SOAP) версии 1.1, расширяемый язык разметки (Extensible Markup Language, XML) и расширяемый язык описания схем данных версии не ниже 1.0 (XML Schema 1.0/1.1). Там же описываются требования к структуре электронных сообщений в СМЭВ.

В связи с многочисленными обращениями пользователей с вопросами по взаимодействию со СМЭВ с использованием наших продуктов мы подготовили ряд примеров, демонстрирующих эту функциональность. В данной статье рассматривается простой способ создания подписанных сообщений для СМЭВ и проверки подписи в сообщениях от СМЭВ с использованием платформы Microsoft .NET Framework.

Данный пример написан на языке C# и основан на использовании методов класса SignedXml. Создание тела сообщения и разбор ответа СМЭВ оставлены за рамками примера. Для работы примера необходимо установкить СКЗИ КриптоПро CSP 3.6 R2 и КриптоПро .NET, а также иметь сертификат подписи (пробный сертификат можно получить на нашем тестовом центре).

Ближе к делу!

За основу взят первый пример из статьи про SignedXml, который создаёт и проверяет приложенную (enveloped) подпись.

На вход необходимо подать документ XML примерно следующего вида:

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"
        xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
        xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <S:Header>
         <wsse:Security S:actor="http://smev.gosuslugi.ru/actors/smev">
             <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                 <ds:KeyInfo>
                     <wsse:SecurityTokenReference>
                         <wsse:Reference URI="#SenderCertificate"/>
                     </wsse:SecurityTokenReference>
                 </ds:KeyInfo>
             </ds:Signature>
             <wsse:BinarySecurityToken EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
                       ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"
                       wsu:Id="SenderCertificate">
             </wsse:BinarySecurityToken>
         </wsse:Security>
     </S:Header>
     <S:Body wsu:Id="body">
      <!-- Здесь идёт тело запроса-->
     </S:Body>
</S:Envelope>

Первое, что необходимо изменить в коде исходного примера, это создание объекта класса SignedXml. Метод GetIdElement этого класса умеет искать узлы только по атрибуту Id в глобальном простанстве имен, тогда как подписываемое тело сообщения для СМЭВ должно быть помечено атрибутом wsu:Id. Для перегрузки этого метода создаём новый класс:

class SmevSignedXml : SignedXml
{
    public SmevSignedXml(XmlDocument document)
        : base(document)
    {
    }
 
    public override XmlElement GetIdElement(XmlDocument document, string idValue)
    {
        XmlNamespaceManager nsmgr = new XmlNamespaceManager(document.NameTable);
        nsmgr.AddNamespace("wsu", WSSecurityWSUNamespaceUrl);
        return document.SelectSingleNode("//*[@wsu:Id='" + idValue + "']", nsmgr) as XmlElement;
    }
}

Заменяем создание объекта:

SmevSignedXml signedXml = new SmevSignedXml(doc);

Ключ подписи возьмём не случайный, как в исходном примере, а из сертификата:

signedXml.SigningKey = Certificate.PrivateKey;

Подписывать будем только один элемент XML, поэтому в ссылке указываем его идентификатор. В данном примере и в методических рекомендациях СМЭВ подписываемый узел soapenv:Body помечен идентификатором "body":

Reference reference = new Reference();
reference.Uri = "#body";

Задаём алгоритм хэширования подписываемого узла - ГОСТ Р 34.11-94. Необходимо использовать устаревший идентификатор данного алгоритма, т.к. именно такой идентификатор используется в СМЭВ:

reference.DigestMethod = 
    CryptoPro.Sharpei.Xml.CPSignedXml.XmlDsigGost3411UrlObsolete;

Преобразование (transform) для создания приложенной подписи в данном примере не нужно. Для СМЭВ необходимо добавить преобразование, приводящее подписываемый узел к каноническому виду по алгоритму http://www.w3.org/2001/10/xml-exc-c14n#:

XmlDsigExcC14NTransform c14 = new XmlDsigExcC14NTransform();
reference.AddTransform(c14);

Задаём преобразование для приведения узла ds:SignedInfo к каноническому виду по алгоритму http://www.w3.org/2001/10/xml-exc-c14n# в соответствии с методическими рекомендациями СМЭВ:

signedXml.SignedInfo.CanonicalizationMethod =
    SignedXml.XmlDsigExcC14NTransformUrl;

Задаём алгоритм подписи - ГОСТ Р 34.10-2001. Необходимо использовать устаревший идентификатор данного алгоритма, т.к. именно такой идентификатор используется в СМЭВ:

signedXml.SignedInfo.SignatureMethod =
    CryptoPro.Sharpei.Xml.CPSignedXml.XmlDsigGost3410UrlObsolete;

После вычисления подписи вместо добавления полученного узла ds:Signature в документ целиком необходимо взять лишь некоторые подузлы и вставить их в заготовленное место:

doc.GetElementsByTagName("ds:Signature")[0].PrependChild(
    doc.ImportNode(xmlDigitalSignature.GetElementsByTagName("SignatureValue")[0], true));
doc.GetElementsByTagName("ds:Signature")[0].PrependChild(
    doc.ImportNode(xmlDigitalSignature.GetElementsByTagName("SignedInfo")[0], true));

Остаётся лишь добавить сертификат подписи в заготовленный узел wsse:BinarySecurityToken:

doc.GetElementsByTagName("wsse:BinarySecurityToken")[0].InnerText =
    Convert.ToBase64String(Certificate.RawData);

Вуаля! Теперь документ doc можно отправлять в СМЭВ любым удобным способом.

Теперь рассмотрим проверку подписи. Для проверки вместо класса SingedXml также необходимо использовать класс SmevSignedXml. После загрузки узла с подписью в соответствующий объект ищем по ссылке узел wsse:BinarySecurityToken, содержащий сертификат подписи:

XmlNodeList referenceList = signedXml.KeyInfo.GetXml().GetElementsByTagName(
    "Reference", WSSecurityWSSENamespaceUrl);
if (referenceList.Count == 0)
{
    throw new XmlException("Не удалось найти ссылку на сертификат");
}
 
// Ищем среди аттрибутов ссылку на сертификат.
string binaryTokenReference = ((XmlElement)referenceList[0]).GetAttribute("URI");
 
// Ссылка должна быть на узел внутри данного документа XML, т.е. она имеет вид
// #ID, где ID - идентификатор целевого узла
if (string.IsNullOrEmpty(binaryTokenReference) || binaryTokenReference[0] != '#')
{
    throw new XmlException("Не удалось найти ссылку на сертификат");
}
 
// Получаем узел BinarySecurityToken с закодированным в base64 сертификатом
XmlElement binaryTokenElement = signedXml.GetIdElement(
    xmlDocument, binaryTokenReference.Substring(1));
if (binaryTokenElement == null)
{
    throw new XmlException("Не удалось найти сертификат");
}

Остаётся лишь создать объект класса X509Certificate2 и проверить подпись с помощью соответствующего открытого ключа:

X509Certificate2 cert =
    new X509Certificate2(Convert.FromBase64String(binaryTokenElement.InnerText));
bool result = signedXml.CheckSignature(cert.PublicKey.Key);

Полный текст примера можно найти в составе КриптоПро .NET SDK.

А что дальше?

Рассмотренный способ выполнения требований по общению со СМЭВ с использованием класса SignedXml прост и понятен, но подготовка запросов и ответов к веб-сервисам СМЭВ, оставленная за рамками примера, может стать дополнительной головной болью для разработчиков. Для упрощения этих этапов предназначены специализированные средства для создания веб-сервисов и клиентских приложений к ним, такие как Windows Communication Foundation (WCF). В WCF встроена поддержка защищённых способов взаимодействия с веб-сервисами, к которым относится в том числе и подпись запросов и ответов, используемая в СМЭВ.

В ближайшее время на страницах нашего блога также будут опубликованы статьи с описанием примеров взаимодействия с сервисами СМЭВ с использованием WCF и средств платформы Java. Так что следите за обновлениями! Проще всего это сделать, подписавшись на RSS-поток этого блога.

Алексей Голдбергс

Павел Смирнов

Максим Коллегин

Михаил Хоменко

* Благодарим наших партнеров, ООО Удостоверяющий центр «АСКОМ» за предоставленные материалы, которые были использованы при написании данной статьи.

Follow us