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

Публикация: 02 Июль 2012 - 12:31, редакция: 20.06.2018 15:33

Недавно в этом блоге мы открыли тему интеграции со СМЭВ с использованием наших продуктов. Прошлая статья посвящена разработке на платформе .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.

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

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

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