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

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

В продолжение темы мы хотим рассмотреть пример взаимодействия со СМЭВ с использованием платформы Java.

Для запуска примера необходимо:

  • установить JRE версий 1.6 или 1.7,
  • скачать WSS4J версии 1.6.3 или 1.6.6,
  • создать проект и подключить библиотеки WSS4J,
  • установить КриптоПро JCP версии 1.0.52 или 1.0.53 в используемую JRE,
  • скачать и подключить сервис-провайдер CryptoProXMLDSigRI,
  • сформировать ключ и сертификат для подписи.

Начнём!

Входящее сообщение выглядит так:

<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"
           xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
      <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>

Предварительно нужно выполнить инициализацию сервис-провайдера для подписи XML:

// Инициализация Transforms.
com.sun.org.apache.xml.internal.security.Init.init();
// Инициализация сервис-провайдера.
if(!JCPXMLDSigInit.isInitialized()) {
    JCPXMLDSigInit.init();
}

Загружаем необходимые для работы данные (ключ, сертификат, сообщение):

// Инициализация ключевого контейнера и получение сертификата и закрытого ключа.
KeyStore keyStore = KeyStore.getInstance(JCP.HD_STORE_NAME);
PrivateKey privateKey = (PrivateKey)keyStore.getKey(ALIAS, PASSWORD);
X509Certificate cert = (X509Certificate)keyStore.getCertificate(ALIAS);
// Подготовка сообщения: в данном случае — это чтение сообщения из файла message.xml в кодировке UTF-8.
MessageFactory mf = MessageFactory.newInstance();
SOAPMessage message = mf.createMessage();
SOAPPart soapPart = message.getSOAPPart();
FileInputStream is = new FileInputStream("message.xml");
soapPart.setContent(new StreamSource(is));
message.getSOAPPart().getEnvelope().addNamespaceDeclaration("ds", "http://www.w3.org/2000/09/xmldsig#");
Document doc = message.getSOAPPart().getEnvelope().getOwnerDocument();

Добавляем заголовки для помещения информации о подписи:

WSSecHeader header = new WSSecHeader();
header.setActor("http://smev.gosuslugi.ru/actors/smev");
header.setMustUnderstand(false);
header.insertSecurityHeader(message.getSOAPPart().getEnvelope().getOwnerDocument());
// Элемент подписи.
Element token = header.getSecurityHeader();

Далее необходимо создать экземпляр сервис-провайдера для подписи документа:

// Загрузка провайдера.
Provider xmlDSigProvider = new ru.CryptoPro.JCPxml.dsig.internal.dom.XMLDSigRI();

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

final Transforms transforms = new Transforms(doc);
transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM", xmlDSigProvider);

Преобразования над узлом ds:SignedInfo:

List<Transform> transformList = new ArrayList<Transform>();
Transform transformC14N = fac.newTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS, (XMLStructure) null);
transformList.add(transformC14N);

Добавляем ссылку на подписываемый узел с идентификатором "body":

// Ссылка на подписываемые данные с алгоритмом хеширования ГОСТ 34.11.
Reference ref = fac.newReference("#body", fac.newDigestMethod("http://www.w3.org/2001/04/xmldsig-more#gostr3411", null),
transformList, null, null);

Задаём алгоритм подписи:

SignedInfo si = fac.newSignedInfo( fac.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE,
(C14NMethodParameterSpec) null), fac.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411",    null), Collections.singletonList(ref));

В качестве алгоритма хэширования был задан алгоритм ГОСТ Р 34.11-94, а алгоритма подписи — ГОСТ Р 34.10-2001.

Создаём узел ds:KeyInfo с информацией о сертификате:

KeyInfoFactory kif = fac.getKeyInfoFactory();
X509Data x509d = kif.newX509Data(Collections.singletonList((X509Certificate) cert));
KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509d));

Подписываем данные в элементе token:

javax.xml.crypto.dsig.XMLSignature sig = fac.newXMLSignature(si, ki);
DOMSignContext signContext = new DOMSignContext((Key) privateKey, token);
sig.sign(signContext);

Следующий этап — поместить узел ds:Signature и сертификат (X509Certificate) в узел wsse:Security, причём сертификат нужно удалить из ds:KeyInfo и оставить там ссылку на wsse:BinarySecurityToken с сертификатом:

// Узел подписи Signature.
Element sigE = (Element) XPathAPI.selectSingleNode(signContext.getParent(), "//ds:Signature");
// Блок данных KeyInfo.
Node keyE = XPathAPI.selectSingleNode(sigE, "//ds:KeyInfo", sigE);
// Элемент SenderCertificate, который должен содержать сертификат.
Element cerVal = (Element) XPathAPI.selectSingleNode(token, "//*[@wsu:Id='SenderCertificate']");
cerVal.setTextContent(XPathAPI.selectSingleNode(keyE, "//ds:X509Certificate", keyE).getFirstChild().getNodeValue());
// Удаляем содержимое KeyInfo
keyE.removeChild(XPathAPI.selectSingleNode(keyE, "//ds:X509Data", keyE));
NodeList chl = keyE.getChildNodes();
for (int i = 0; i < chl.getLength(); i++) {
    keyE.removeChild(chl.item(i));
}
// Узел KeyInfo содержит указание на проверку подписи с помощью сертификата SenderCertificate.
Node str = keyE.appendChild(doc.createElementNS("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "wsse:SecurityTokenReference"));
Element strRef = (Element)str.appendChild(doc.createElementNS("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "wsse:Reference"));
strRef.setAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3");
strRef.setAttribute("URI", "#SenderCertificate");
header.getSecurityHeader().appendChild(sigE);

Теперь документ готов к отправке.

Семь раз проверь, один — отрежь.

Резать мы конечно ничего не будем, но вот проверить подпись в сообщении из СМЭВ перед его обработкой необходимо. Хотя бы один раз.

Проверка подписи производится следующим образом:

// Получение узла, содержащего сертификат.
final Element wssecontext = doc.createElementNS(null, "namespaceContext");
wssecontext.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
NodeList secnodeList = XPathAPI.selectNodeList(doc.getDocumentElement(), "//wsse:Security");
// Поиск элемента сертификата в блоке BinarySecurityToken.
Element r = null;
Element el = null;
if( secnodeList != null&&secnodeList.getLength()>0 ) {
    String actorAttr = null;
    for( int i = 0; i<secnodeList.getLength(); i++ ) {
        el = (Element) secnodeList.item(i);
        actorAttr = el.getAttributeNS("http://schemas.xmlsoap.org/soap/envelope/", "actor");
        if(actorAttr != null&&actorAttr.equals("http://smev.gosuslugi.ru/actors/smev")) {
            r = (Element)XPathAPI.selectSingleNode(el,    "//wsse:BinarySecurityToken[1]", wssecontext);
            break;
        }
    }
}
if(r == null) {
    return;
}
// Получение сертификата.
final X509Security x509 = new X509Security(r);
// Создаем сертификат.
cert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(x509.getToken()));
if (cert == null) {
    throw new Exception("Сертификат не найден.");
}
System.out.println("Verify by: " + cert.getSubjectDN());
// Поиск элемента Signature.
NodeList nl = doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature");
if (nl.getLength() == 0) {
    throw new Exception("Не найден элемент Signature.");
}
// Задаем открытый ключ для проверки подписи.
fac = XMLSignatureFactory.getInstance("DOM", xmlDSigProvider);
DOMValidateContext valContext = new DOMValidateContext(KeySelector.singletonKeySelector(cert.getPublicKey()), nl.item(0));
javax.xml.crypto.dsig.XMLSignature signature = fac.unmarshalXMLSignature(valContext);
// Проверяем подпись и выводим результат проверки.
System.out.println( "Verified: " + signature.validate(valContext));

Сертификат в примере извлекается из узла wsse:BinarySecurityToken, и его открытый ключ используется для проверки подписи.

Полный текст примера и необходимые библиотеки можно скачать по ссылке.

To be continued...

В следующей серии мы вернёмся в мир .NET, где вас ждёт пример работы со СМЭВ с использованием WCF.

Афанасьев Евгений

Беляев Анатолий

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

Никак не получается подружить SMEVExample из примера с токеном

Сначала ошибся и пробовал настроить на eToken.
Исправился, доставил всё что нужно. В контрольной панели от jcp видит и токен и ключевой контейнер и серт. Не понятно что нужно сконфигурировать в примере, чтобы он смог обратиться к токену и подписать сообщение.

Здравствуйте. На ваш вопрос

Здравствуйте.
На ваш вопрос ответили на форуме https://www.cryptopro.ru/forum2/default.aspx?g=posts&t=10347

.NET пример работы со СМЭВ использованием WCF

"В следующей серии мы вернёмся в мир .NET, где вас ждёт пример работы со СМЭВ с использованием WCF. "

Не подскажете, когда будет следующая серия? С нетерпением ждем.
Спасибо.

Запуск примера

Добрый день!!!

Подскажите, пожалуйста, что я делаю не так:

в туториале указано:
"поместить в папку src папку wss4j со всеми содержащимися в ней папками и файлами. В свойствах проекта следует добавить путь к XMLDSigRI.jar и jar-файлам wss4j 1.6.3"

в папку wss4j нет jar-файлов.. где их взять??

Неудачный запуск примера

Добрый день!!

помогите, пожалуйста, разобраться, что я делаю не так.

скачал архив с примерами. сделал все, как написано в "README.jcp+wss4j_1.6.3+xmldsigri.txt", но...

1. не нашел файлов axis-1.4.jar axis-jaxrpc-1.4.jar bcmail-jdk16-146.jar
2. при попытке скомпилировать SMEVExample.java компилятор выдает ошибку:
error: package com.sun.org.apache.xml.internal.security does not exist com.sun.org.apache.xml.internal.security.Init.init();

где взять jar-файл, содержащий пакет com.sun.org.apache.xml.internal.security.Init?

Perl

А как осуществить подписание и проверку подписи средствами Perl? Какие способы соединения Perl c CryptoPro посоветуете?

Вызов cryptcp из командной строки

Я думаю, те же, что и в PHP вызов cryptcp в коммандной строке через exec или system...

JCP + Oracle WebLogic 10.3.6

Спасибо за туториал.
В standalone-приложении все работает отлично. Кто нибудь пробовал реализовывать подпись подобным образом в WebLogic'е?
Просто я тут получаю java.lang.AssertionError: UNIMPLEMENTED.
P.S. Некоторое время назад создал топик на форуме (http://www.cryptopro.ru/forum2/default.aspx?g=posts&t=5956), но что-то пока никто не откликнулся.

Чистый wss4j

В примере wss4j используется вперемешку с более низкоуровневым JSR 105 API, что выглядит странно. Зачем? Можно ли достичь тех же целей, используя только вызовы wss4j?
Если да, то откроется возможность для интеграции Крипто-Про JCP с Axis2 и CXF.

Apache CXF и ЭЦП для SOAP сообщений СМЭВ

http://oldcouncil.blogspot.ru/2013/03/apache-cxf-soap.html

Я думаю это то, о чем Вы спрашивали.

java.lang.NoClassDefFoundError: org/apache/xml/security/c14n/Inv

Пытаюсь применить код из статьи. Использую Spring + CXF + JCP 1.54 +wss4j 1.6.11. Вываливается эксепшн из заголовка. Пробовал версии wss4j 1.6.3 -1.6.6 Тогда даже не компилируется из-за отсутствия методов использованых в XmlDSignTools. У кого-то получилось подписать с использованием Spring?

лицензии

какая лицензия(клиентская на рабочее место или полноценная серверная) необходима для взаимодействия со СМЭВ с помощью JCP?

Аренда ЭЦП

Будет ли правомерна такая ситуация (и почему): физическое лицо формирует ЭЦП при помощи вашего программного средства (КриптоПро) и на основании письменной договоренности с юридическим лицом использует своё ЭЦП в интересах юр.лица (получается что-то типа аренды ЭЦП)?

Аренда ЭЦП

Это, видимо, только в нашей стране возможно.

Купить

Форма заказа

Вход

Подписка на обновления