вторник, 1 октября 2013 г.

Подписывание XML документов

Подписывание XML документа

В предыдущей статье, Apache CXF и ЭЦП для SOAP сообщений СМЭВ, я рассказал о том, как можно средствами Apache CXF подписывать SOAP сообщение. Подпись появляется в заголовке SOAP сообщения.

Интересный вопрос появляется, когда передаваемый документ должен содержать подпись. Если рассматривать в рамках системы СМЭВ, передаваемый SOAP запрос будет содержать две подписи. Первая подпись внутри тела сообщения (envelope->body). Вторая подпись в заголовке сообщения (envelope->header).

Реализация

По своей сути, этот пример является переработанной частью системы подписывания SOAP сообщения, реализованная в WSS4JOutInterceptor. Добавлен самый интересный фрагмент - поиск и удаление старых подписей.

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.xml.sign;

public interface SoapClient {
   public static final String SIGN_PROVIDER = "..."; // название провайдера JCA ГОСТ 
   public static final String XML_PROVIDER = "..."; // название провайдера JSR-105 XMLDSIGN
   public static final String CRYPTO_GOST_DIGEST = "http://www.w3.org/2001/04/xmldsig-more#gostr3411";
   public static final String CRYPTO_GOST_SIGN = "http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411";
}
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.xml.sign;

import java.security.*;
import java.util.*;
import javax.xml.crypto.dsig.*;
import org.apache.ws.security.components.crypto.CryptoType;
import org.w3c.dom.*;

/**
 * Система подписывания XML документа
 * 
 * @author Aleksey Sushko
 */
public class Signer {
    private Properties cryptoSign;
    private String signKeyAlias;
    private XMLSignatureFactory signatureFactory;

    public void createSigner(String signKeyAlias, Properties properties) throws NoSuchProviderException {
        this.signKeyAlias = signKeyAlias;
        cryptoSign = new Properties();
  
        for(Object propKey : properties.keySet()) {
            String key = propKey.toString();
            String val = properties.getProperty(key);
   
            if(val != null) {
                if(key.startsWith("cryptoSign.") && key.length() > 11) {
                    cryptoSign.setProperty(key.substring(11), val);
                }
            }
        }
        signatureFactory = XMLSignatureFactory.getInstance("DOM", XML_PROVIDER);
    }

    public void sign(Document document) throws Exception {
        LocalMerlin crypto = new LocalMerlin(cryptoSign); 
        PrivateKey privateKey = crypto.getPrivateKey(signKeyAlias, null);
        CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
        cryptoType.setAlias(signKeyAlias);
        X509Certificate[] certs = crypto.getX509Certificates(cryptoType);
  
        // ищем старые разделы ЭЦП
        NodeList nl = document.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
        if(nl.getLength() != 0) {
            // Убираем все старые подписи
            for(int i = nl.getLength() - 1; i >= 0; i--) {
                Node item = nl.item(i);
                Node parent = item.getParentNode();
                parent.removeChild(item);
            }
        }
  
        // приведение документа к каноническому виду
        CanonicalizationMethod c14nMethod = signatureFactory.newCanonicalizationMethod(
            CanonicalizationMethod.EXCLUSIVE, 
            (C14NMethodParameterSpec) null);
        // не подписывать другие подписи 
        Transform transform_1 = signatureFactory.newTransform(
            Transform.ENVELOPED, 
            (TransformParameterSpec) null);
        Transform transform_2 = signatureFactory.newTransform(
            CanonicalizationMethod.EXCLUSIVE, 
            (TransformParameterSpec) null);
        // аглоритм расчета хеш функции (digest GOST3411)
        DigestMethod digestMethod = signatureFactory.newDigestMethod(
            SoapClient.CRYPTO_GOST_DIGEST, 
            null);
        // алгоритм подписи (GOST 3410-2001)
        SignatureMethod signatureMethod = signatureFactory.newSignatureMethod(
            SoapClient.CRYPTO_GOST_SIGN, 
            null);
        // указываем подписываемые данные
        Reference ref = signatureFactory.newReference("", 
            digestMethod,
            Arrays.asList(transform_1, transform_2),
            null, null);
        // информационный блок подписи
        SignedInfo signedInfo = signatureFactory.newSignedInfo(
            c14nMethod,
            signatureMethod,
            Collections.singletonList(ref));

        // сведения о ключе, которым подписываются данные
        KeyInfoFactory keyInfoFactory = signatureFactory.getKeyInfoFactory();
        X509Data x509Data = keyInfoFactory.newX509Data(Arrays.asList(certs));
        KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(x509Data));

        // создаем блок подписи
        XMLSignature signature = signatureFactory.newXMLSignature(signedInfo, keyInfo);
        DOMSignContext domSignContext = new DOMSignContext(privateKey, document.getDocumentElement());
  
        // устанавливаем нужного поставщика подписей, который добыл закрытый ключ для подписи
        domSignContext.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", 
            Security.getProvider(SoapClient.SIGN_PROVIDER));
  
        // подписываем документ
        signature.sign(domSignContext);
    }
}

Применение

Надо подумать о том, как этим функционалом правильно пользоваться. По сути, подписать XML документ может одна система, а передать SOAP сообщение другая система. Это означает, что подписи могут быть получены на разных ключах.

Рассматривая структуру XML документов системы СМЭВ, мы видим вставку блока Signature во все документы. Это значит, что взяв за основу JAXB, и преобразовав подписанный XML документ, мы получим набор Java объектов с информацией о подписи. Выполнив обратное JAXB преобразование, мы получим прежний подписанный XML документ. Указанные методы преобразования CanonicalizationMethod и Transform дадут те же самые данные, что были у исходного XML документа.

Получается, что мы можем заполнить POJO объект данными, преобразовать в XML представление, прогнать через подписание, свернуть в новый POJO объект. Полученный объект передать в CXF SopaClient. Apache CXF вернет POJO объект в XML представление, вставит его в тело SOAP запроса, подпишет сообщение и отправит на сервер.

Остается дело за малым - нужно правильно приготовить WSS4JInInterceptor для проверки подписей принимаемых ответов. Реализовав обе части, WSS4JOutInterceptor и WSS4JInInterceptor, можно смело браться за реализацию собственной СМЭВ службы.

На затравку

Я начинал рассказывал о том, как использовать JCA провайдера КриптоПро JCP для работы с Apache CXF. Скажу так, что взяв исходный материала OpenJava JDK, SunMSCAPI провайдера, можно сделать свой мост через MS Crypto API к КриптоПро CSP. Надо знать самое главное - в Oracle JCA провайдере можно легко реализовать расчет хеш функций, подпись, проверку подписи, контейнер сертификатов и ключей. Нельзя сделать только шифрование и расшифровку данных. Для этого надо получить сертификат Oracle для подписывания библиотек криптографического провайдера.