воскресенье, 3 марта 2013 г.

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

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

Если мы любители Java и столкнулись с СМЭВ, то первым делом задумываемся о WS-Security. Это электронная цифровая подпись в передаваемых SOAP сообщениях.

И вот, мы открываем поисковый сервер, набираем запрос и попадаем в блог компании КриптоПро. Там описывается методика формирования подписи в SOAP сообщении. Я, как только увидел этот опус, сразу понял, что тут народ что-то замутил.

Первое, что бросается в глаза, это использование WSS4J, Axis2. Предположим, что в IBM WebSphere, может и сейчас используется стек Axis2. В JBoss AS 7.1.1, Apache Camel, да и в Apache Karaf (OSGi), мы видим другой стек - Apache CXF.

Открыв документацию на IBM сайте, http://www.ibm.com/developerworks/ru/library/j-jws13/index.html , мы видим довольно добротную документацию по подписыванию и шифрованию SOAP сообщений. Взглянув на блог http://www.cryptopro.ru/blog/2012/07/02/podpis-soobshchenii-soap-dlya-smev-s-ispolzovaniem-kriptopro-jcp , я ужаснулся тому, как всё замешано. Тут нам и WSS4J API, и SOAP Message API. А где же прелести CXF с минимальными настройками?
И так приступим к разбору.

Доступ к хранилищу ключей и сертификатов

Настройка WSSj4 заключается в правильном описании файла свойств crypto.properties
org.apache.ws.security.crypto.provider=org.company.soap.impl.ws.security.components.crypto.LocalMerlin
org.apache.ws.security.crypto.merlin.keystore.provider=JCP
org.apache.ws.security.crypto.merlin.keystore.type=HDImageStore

В WSSj4 нам предлагается криптографический компонент Merlin. Я его заменяю на собственную локализацию LocalMerlin. Причина заключается в работе JCP с хранилищем закрытых ключей HDImageStore. По умолчанию, базовый Merlin должен получить доступ к файлу хранилища. Но в JCP, базовое хранилище, это некий каталог файловой системы. Соответственно, Merlin, не откроет хранилище.

Вторая особенность JCP хранилища HDImageStore. Если мы сделаем хранение контейнера ключа без пароля, мы должны передать null в качестве значения пароля. Merlin вместо этого передает массив нулевой длины.

/**
 * 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.impl.ws.security.components.crypto;

import java.io.IOException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.util.Enumeration;
import java.util.Properties;

import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.components.crypto.CredentialException;
import org.apache.ws.security.components.crypto.Merlin;

/**
 * Замена базового провайдера работы с хранилищами сертификатов и ключей.
 * Цель - использование особенностей работы c хранилищем закрытых ключей JCP (HDImageStore).
 *  
 * @author Aleksey Sushko
 */
public class LocalMerlin extends Merlin {

 private static final String HD_IMAGE_STORE = "HDImageStore";
 private static final org.apache.commons.logging.Log LOG = 
   org.apache.commons.logging.LogFactory.getLog(Merlin.class);

 /**
  * Дубликат закрытой функции из базового класса.
  */
 private static String createKeyStoreErrorMessage(KeyStore keystore) throws KeyStoreException {
  Enumeration<String> aliases = keystore.aliases();
  StringBuilder sb = new StringBuilder(keystore.size() * 7);
  boolean firstAlias = true;
  while (aliases.hasMoreElements()) {
   if (!firstAlias) {
    sb.append(", ");
   }
   sb.append(aliases.nextElement());
   firstAlias = false;
  }
  String msg = " in keystore of type [" + keystore.getType()
    + "] from provider [" + keystore.getProvider()
    + "] with size [" + keystore.size() + "] and aliases: {"
    + sb.toString() + "}";
  return msg;
 }
 
 
 public LocalMerlin() {
  super();
 }

 public LocalMerlin(Properties properties, ClassLoader loader)
   throws CredentialException, IOException {
  super(properties, loader);
 }

 public LocalMerlin(Properties properties) throws CredentialException,
   IOException {
  super(properties);
 }

 /**
  * Базовый Merlin не загружает хранилище личных ключей и доверенных сертификатов,
  * если файл не указан.
  * Цель - при отсутствии имени файла, система поднимает хранилище личных ключей.
  */
 @Override
 public void loadProperties(Properties properties, ClassLoader loader)
   throws CredentialException, IOException {
  super.loadProperties(properties, loader);
  
  String provider = properties.getProperty(CRYPTO_KEYSTORE_PROVIDER);
  if (provider != null)
   provider = provider.trim();
  
  if(keystore == null) {
   String passwd = properties.getProperty(KEYSTORE_PASSWORD, "security");
   String type = properties.getProperty(KEYSTORE_TYPE, KeyStore.getDefaultType());
   
   if(passwd != null)
    passwd = passwd.trim();
   if(type != null)
    type = type.trim();
   
   if(HD_IMAGE_STORE.equals(type))
    keystore = load(null, passwd, provider, type);
  }
  
  if(truststore == null) {
   String passwd = properties.getProperty(TRUSTSTORE_PASSWORD, "changeit");
   String type = properties.getProperty(TRUSTSTORE_TYPE, KeyStore.getDefaultType());
   
   if(passwd != null)
    passwd = passwd.trim();
   if(type != null)
    type = type.trim();
   
   if(HD_IMAGE_STORE.equals(type))
    truststore = load(null, passwd, provider, type);
  }
 }

 /**
  * Gets the private key corresponding to the identifier.
  *
  * @param identifier The implementation-specific identifier corresponding to the key
  * @param password The password needed to get the key
  * @return The private key
  */
 @Override
 public PrivateKey getPrivateKey(
   String identifier,
   String password
   ) throws WSSecurityException {
  if (keystore == null) {
   throw new WSSecurityException("The keystore is null");
  }
  try {
   if (identifier == null || !keystore.isKeyEntry(identifier)) {
    String msg = "Cannot find key for alias: [" + identifier + "]";
    String logMsg = createKeyStoreErrorMessage(keystore);
    LOG.error(msg + logMsg);
    throw new WSSecurityException(msg);
   }
   if (password == null && privatePasswordSet) {
    password = properties.getProperty(KEYSTORE_PRIVATE_PASSWORD);
    if (password != null) {
     password = password.trim();
    }
   }
   
   Key keyTmp = loadPrivateKey(identifier, password);
   if (!(keyTmp instanceof PrivateKey)) {
    String msg = "Key is not a private key, alias: [" + identifier + "]";
    String logMsg = createKeyStoreErrorMessage(keystore);
    LOG.error(msg + logMsg);
    throw new WSSecurityException(msg);
   }
   return (PrivateKey) keyTmp;
  } catch (KeyStoreException ex) {
   throw new WSSecurityException(
     WSSecurityException.FAILURE, "noPrivateKey", new Object[]{ex.getMessage()}, ex
     );
  } catch (UnrecoverableKeyException ex) {
   throw new WSSecurityException(
     WSSecurityException.FAILURE, "noPrivateKey", new Object[]{ex.getMessage()}, ex
     );
  } catch (NoSuchAlgorithmException ex) {
   throw new WSSecurityException(
     WSSecurityException.FAILURE, "noPrivateKey", new Object[]{ex.getMessage()}, ex
     );
  }
 }

 /**
  * Получение закрытого ключа из хранилища.
  * Для хранилища HDImageStore пустой пароль передается как null.
  * Базовый вариант передает пустой массив char[]{}. 
  */
 protected Key loadPrivateKey(String identifier, String password)
   throws KeyStoreException, NoSuchAlgorithmException,
   UnrecoverableKeyException {
  
  char[] passwordArray = null;
  
  if(password == null) { 
   String type = keystore.getType();
   if(HD_IMAGE_STORE.equals(type))
    passwordArray = null;
   else
    passwordArray = new char[]{};
  }
  else {
   passwordArray = password.toCharArray();
  }
  
  Key keyTmp = keystore.getKey(identifier, passwordArray);
  
  return keyTmp;
 }
}

Теперь у нашей реализации WSS4J есть доступ к базовому хранилищу личных ключей JCP HDImageStore. Идем дальше.

Инициализация XMLDSIGN

На форуме КриптоПро, и в документации, нам объясняют, что в каталог $JRE_HOME/lib/ext мы установили много библиотек для работы JCP провайдера. В том числе и библиотеку JCPxml.jar.

Теперь, мы самостоятельно должны положить туда же xmldsign.jar, wss4j.jar, xalan.jar, common-loggin.jar и еще кучу библиотек. Но позвольте, в CXF стеке того же JBoss AS 7.1.1 уже есть весь выше перечисленный набор. Да и набор всех библиотек JCP провайдера, нам приходится вкладывать в каталог lib нашего EAR приложения.

Что-то тут не так. Проведем эксперимент, убираем из каталога $JRE_HOME/lib/ext библиотеку JCPxml.jar. Пропишим ее в maven зависимости локального проекта. Туда же пропишем и библиотеку JCP XMLDSigRI.jar

        <dependency>
            <groupId>crypto-pro</groupId>
            <artifactId>JCPxml</artifactId>
            <version>${cryptopro-jcp.version}</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/JCPxml.jar</systemPath>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>crypto-pro</groupId>
            <artifactId>XMLDSigRI</artifactId>
            <version>${cryptopro-jcp.version}</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/XMLDSigRI.jar</systemPath>
            <type>jar</type>
        </dependency>
Запускаем тест и получаем ошибку. Метод JCPXMLDSigInit.isInitialized() не может найти класс исключения org.apache.xml.security.exceptions.AlgorithmAlreadyRegisteredException. Очень странно видеть это на stanalone application (JUnit test).
if(!JCPXMLDSigInit.isInitialized()){
    JCPXMLDSigInit.init();
}

Делать не чего, надо писать собственную реализацию поднятия XMLDSIGN. Если выполнить рекомендации КриптоПро, то мы можем получить кучу аналогичных ошибок в других местах WSS4J, DOM, SAX, XSLT стека по всем проектам, работающим на боевом сервере приложений.

Поднимаем свою реализацию

/**
 * 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.impl.xml.security;

import java.security.Provider;
import java.security.Security;

import org.apache.ws.security.WSSConfig;
import org.apache.xml.security.algorithms.JCEMapper;
import org.apache.xml.security.algorithms.SignatureAlgorithm;
import org.apache.xml.security.exceptions.AlgorithmAlreadyRegisteredException;
import org.apache.xml.security.signature.XMLSignatureException;
import org.apache.xml.security.utils.Constants;

import org.company.soap.impl.xml.security.algorithms.SignatureGostR34102001Gostr3411;
import org.company.soap.impl.xml.security.algorithms.SignatureGostR34102001URN;

/**
 * Инициализация XMLDSIGN.
 * 
 * @author Aleksey Sushko
 *
 */
public class XmlDSignTools {
 public static final String URL_V1_ALGORITHM_DIGEST = Constants.MoreAlgorithmsSpecNS + "gostr3411";
 public static final String URL_V2_ALGORITHM_DIGEST = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr3411";
 public static final String URL_V1_ALGORITHM_SIGNATURE = Constants.MoreAlgorithmsSpecNS + "gostr34102001-gostr3411";
 public static final String URL_V2_ALGORITHM_SIGNATURE = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102001-gostr3411";
 public static final String URL_V1_ALGORITHM_ENCRIPTION = Constants.MoreAlgorithmsSpecNS + "gost28147";
 public static final String URL_V2_ALGORITHM_ENCRIPTION = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gost28147";
 
 public static final String JCENAME_ALGORITHM_DIGEST = "GOST3411";
 public static final String JCENAME_ALGORITHM_SIGNATURE = "GOST3411withGOST3410EL";
 public static final String JCENAME_ALGORITHM_ENCRIPTION = "GostJCE/CBC/ISO10126Padding";
 
 public static void init() throws AlgorithmAlreadyRegisteredException, XMLSignatureException, ClassNotFoundException {
  org.apache.xml.security.Init.init();
  
  // CryptoPro MessageDigest
  JCEMapper.Algorithm digest = new JCEMapper.Algorithm("", JCENAME_ALGORITHM_DIGEST, "MessageDigest");
  JCEMapper.register(URL_V1_ALGORITHM_DIGEST, digest);
  JCEMapper.register(URL_V2_ALGORITHM_DIGEST, digest);
  
  // CryptoPro Signature
  JCEMapper.Algorithm signature = new JCEMapper.Algorithm("", JCENAME_ALGORITHM_SIGNATURE, "Signature");
  JCEMapper.register(URL_V1_ALGORITHM_SIGNATURE, signature);
  JCEMapper.register(URL_V2_ALGORITHM_SIGNATURE, signature);
  
  // CryptoPro BlockEncryption
  JCEMapper.Algorithm encryption = new JCEMapper.Algorithm("GOST28147", JCENAME_ALGORITHM_ENCRIPTION, "BlockEncryption", 256);
  JCEMapper.register(URL_V1_ALGORITHM_ENCRIPTION, encryption);
  JCEMapper.register(URL_V2_ALGORITHM_ENCRIPTION, encryption);
  
  SignatureAlgorithm.register(URL_V1_ALGORITHM_SIGNATURE, SignatureGostR34102001Gostr3411.class);
  SignatureAlgorithm.register(URL_V2_ALGORITHM_SIGNATURE, SignatureGostR34102001URN.class);
  
  Provider jceProvider = loadJceProvider();
  WSSConfig.appendJceProvider(jceProvider.getName(), jceProvider);
 }
 
 public static Provider loadJceProvider() {
  Provider jceProvider = Security.getProvider("CryptoProXMLDSig");
  if(jceProvider == null) {
   jceProvider = new ru.CryptoPro.JCPxml.dsig.internal.dom.XMLDSigRI();
   Security.addProvider(jceProvider);
  }
  return jceProvider;
 }
 
 public static void initProvider(String providerName, String className) throws Exception {
  Provider provider = Security.getProvider(providerName);
  if(provider == null) {
   loadProvider(className);
  }
 }
 
 private static void loadProvider(String className) throws Exception {
  ClassLoader loader = Thread.currentThread().getContextClassLoader();
  Class<?> c = loader.loadClass(className);

  java.security.Provider newProvider = (java.security.Provider) c.newInstance();
  java.security.Security.addProvider(newProvider);
 }
}
За основу был взят класс описания DSA подписи - SignatureDSA.
/**
 * 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.impl.xml.security.algorithms;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.AlgorithmParameterSpec;

import org.apache.xml.security.algorithms.JCEMapper;
import org.apache.xml.security.algorithms.SignatureAlgorithmSpi;
import org.apache.xml.security.signature.XMLSignatureException;
import org.apache.xml.security.utils.Base64;

/**
 * Базовый класс реализации алгоритма ГОСТ ЭЦП.
 * 
 * @author Aleksey Sushko
 */
abstract class SignatureGostR34102001 extends SignatureAlgorithmSpi {

 /** {@link org.apache.commons.logging} logging facility */
 static org.apache.commons.logging.Log log = 
  org.apache.commons.logging.LogFactory.getLog(SignatureGostR34102001.class.getName());

 /** Field algorithm */
 private java.security.Signature _signatureAlgorithm = null;

 /**
  * Constructor SignatureGostR34102001
  *
  * @throws XMLSignatureException
  */
 SignatureGostR34102001() throws XMLSignatureException {
  String algorithmID = JCEMapper.translateURItoJCEID(engineGetURI());
  if (log.isDebugEnabled())
   log.debug("Created SignatureGostr34102001Gostr3411 using " + algorithmID);

  String provider = JCEMapper.getProviderId();
  try {
   if (provider == null) {
    this._signatureAlgorithm = Signature.getInstance(algorithmID);
   } else {
    this._signatureAlgorithm = 
     Signature.getInstance(algorithmID, provider);
   }
  } catch (java.security.NoSuchAlgorithmException ex) {
   Object[] exArgs = { algorithmID, ex.getLocalizedMessage() };
   throw new XMLSignatureException("algorithms.NoSuchAlgorithm", exArgs);
  } catch (java.security.NoSuchProviderException ex) {
   Object[] exArgs = { algorithmID, ex.getLocalizedMessage() };
   throw new XMLSignatureException("algorithms.NoSuchAlgorithm", exArgs);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineSetParameter(AlgorithmParameterSpec params) throws XMLSignatureException {
  try {
   this._signatureAlgorithm.setParameter(params);
  } catch (InvalidAlgorithmParameterException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected boolean engineVerify(byte[] signature) throws XMLSignatureException {
  try {
   if (log.isDebugEnabled())
    log.debug("Called gostr34102001-gostr3411.verify() on " + Base64.encode(signature));

   return this._signatureAlgorithm.verify(signature);
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  } 
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineInitVerify(Key publicKey) throws XMLSignatureException {
  if (!(publicKey instanceof PublicKey)) {
   String supplied = publicKey.getClass().getName();
   String needed = PublicKey.class.getName();
   Object exArgs[] = { supplied, needed };

   throw new XMLSignatureException
   ("algorithms.WrongKeyForThisOperation", exArgs);
  }

  try {
   System.out.println("engineInitVerify publicKey is " + publicKey.getClass());
   
   this._signatureAlgorithm.initVerify((PublicKey) publicKey);
  } catch (InvalidKeyException ex) {
   // reinstantiate Signature object to work around bug in JDK
   // see: http://bugs.sun.com/view_bug.do?bug_id=4953555
   Signature sig = this._signatureAlgorithm;
   try {
    this._signatureAlgorithm = Signature.getInstance
    (_signatureAlgorithm.getAlgorithm(), _signatureAlgorithm.getProvider().getName());
   } catch (Exception e) {
    // this shouldn't occur, but if it does, restore previous
    // Signature
    if (log.isDebugEnabled()) {
     log.debug("Exception when reinstantiating Signature:" + e);
    }
    this._signatureAlgorithm = sig;
   }
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected byte[] engineSign() throws XMLSignatureException {
  try {
   byte jcebytes[] = this._signatureAlgorithm.sign();
   
   return jcebytes;
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineInitSign(Key privateKey, SecureRandom secureRandom)
 throws XMLSignatureException {
  if (!(privateKey instanceof PrivateKey)) {
   String supplied = privateKey.getClass().getName();
   String needed = PrivateKey.class.getName();
   Object exArgs[] = { supplied, needed };

   throw new XMLSignatureException("algorithms.WrongKeyForThisOperation", exArgs);
  }

  try {
   this._signatureAlgorithm.initSign((PrivateKey) privateKey,
     secureRandom);
  } catch (InvalidKeyException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineInitSign(Key privateKey) throws XMLSignatureException {
  if (!(privateKey instanceof PrivateKey)) {
   String supplied = privateKey.getClass().getName();
   String needed = PrivateKey.class.getName();
   Object exArgs[] = { supplied, needed };

   throw new XMLSignatureException("algorithms.WrongKeyForThisOperation", exArgs);
  }

  try {
   this._signatureAlgorithm.initSign((PrivateKey) privateKey);
  } catch (InvalidKeyException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineUpdate(byte[] input) throws XMLSignatureException {
  try {
   this._signatureAlgorithm.update(input);
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineUpdate(byte input) throws XMLSignatureException {
  try {
   this._signatureAlgorithm.update(input);
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineUpdate(byte buf[], int offset, int len)
 throws XMLSignatureException {
  try {
   this._signatureAlgorithm.update(buf, offset, len);
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * Method engineGetJCEAlgorithmString
  *
  * @inheritDoc
  */
 @Override
 protected String engineGetJCEAlgorithmString() {
  return this._signatureAlgorithm.getAlgorithm();
 }

 /**
  * Method engineGetJCEProviderName
  *
  * @inheritDoc
  */
 @Override
 protected String engineGetJCEProviderName() {
  return this._signatureAlgorithm.getProvider().getName();
 }

    /**
     * Method engineSetHMACOutputLength
     *
     * @param HMACOutputLength
     * @throws XMLSignatureException
     */
 @Override
    protected void engineSetHMACOutputLength(int HMACOutputLength)
            throws XMLSignatureException {
        throw new XMLSignatureException(
     "algorithms.HMACOutputLengthOnlyForHMAC");
    }

    /**
     * Method engineInitSign
     *
     * @param signingKey
     * @param algorithmParameterSpec
     * @throws XMLSignatureException
     */
 @Override
    protected void engineInitSign(
        Key signingKey, AlgorithmParameterSpec algorithmParameterSpec)
            throws XMLSignatureException {
        throw new XMLSignatureException(
            "algorithms.CannotUseAlgorithmParameterSpecOnDSA");
    }
 
}
/**
 * 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.impl.xml.security.algorithms;

import org.apache.xml.security.signature.XMLSignatureException;
import org.apache.xml.security.utils.Constants;

public class SignatureGostR34102001Gostr3411 extends SignatureGostR34102001 {

 /** Field _URI */
 public static final String _URI = Constants.MoreAlgorithmsSpecNS + "gostr34102001-gostr3411";

 @Override
 protected String engineGetURI() {
  return SignatureGostR34102001Gostr3411._URI;
 }

 /**
  * Constructor SignatureGostr34102001Gostr3411
  *
  * @throws XMLSignatureException
  */
 public SignatureGostR34102001Gostr3411() throws XMLSignatureException {
 }
}
/**
 * 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.impl.xml.security.algorithms;

import org.apache.xml.security.signature.XMLSignatureException;

public class SignatureGostR34102001URN extends SignatureGostR34102001 {
 /** Field _URI */
 public static final String _URI = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102001-gostr3411";

 @Override
 protected String engineGetURI() {
  return SignatureGostR34102001URN._URI;
 }

 /**
  * Constructor SignatureGost34102001URN
  *
  * @throws XMLSignatureException
  */
 public SignatureGostR34102001URN() throws XMLSignatureException {
 }
}

Настройка WSS4J

Для старого WSS4J этого кода, должно было хватить. Но на свет вышли версии 1.6.x и в них появился очередной поставщик XML трансформаций. Делать нечего, берем библиотеку JCP XMLDSigRI.jar. Правда, сама КриптоПро, толком не объясняет что это такое. Хотите использовать WSS4J 1.6.x, берите, а не хотите - не будет работать и всё тут.

Постараюсь ввести в курс дела. В старой версии Apache Santuario XMLDSIGN существовал XML файл с описанием разных видов трансформации XML документов, алгоритмов расчета хеш функций (MessageDigest), алгоритмов подписи и шифрования. Была одна неприятная особенность всего этого дела. Выражалась она в том, что Apache Santuario один раз инициализировал себя и не было возможности добавить собственные правила. Например, для того же ГОСТ ЭЦП.

Был только один механизм - установить системную переменную, в которой указать модернизированный xml документ. Если у нас не сервер приложений, а обычное приложение, есть шанс самостоятельно указать xml файл, для инициализации Apache Santuario.

Позвольте, но при чем тут WSS4J, если описание идет о реализации XMLDSIGN? Ответ жутко не интересный. В коде проекта WSS4J можно указать поставщика базовой криптографии, реализующего работу с сертификатами, ключами и алгоритмами. Поставщик XML преобразований зашит намертво - ApacheXMLDSig.

Нет, конечно же, можно как-то обойти инициализацию этого провайдера. Только, внутри статического метода init() класса WSSConfig идет прямой вызов статического метода addXMLDSigRIInternal(), который самым безобразным образом поднимает поставщика ApacheXMLDSig.

Короче, не нужен нам ApacheXMLDSig, будьте любезны, перепишите свою реализацию WSS4J под себя. Моя цель не ломать имеющуюся реализацию сторонних библиотек, а настроить систему под Российские реалии.

И так, мне понадобится найти классы, где идет обращение к поставщику XML трансформаций и заменить на использование того, что дает КриптоПро в библиотеке XMLDSigRI.jar.

Приступим. Находим название класса поставщика от КриптоПро. Тут изобретать ничего не пришлось. Имена классов, до буквы, совпадают с классами из XMLDSIGN. В JUnit тесте поднимаем объект поставщика и запрашиваем у него название.

За подписывание SOAP сообщения, в WSS4J отвечает класс WSSecSignature. Конструкторы данного класса вызывают защищенный метод init(), который, подобно собаке, мертвой хваткой цепляется за поставщика ApacheXMLDSig. Делаем собственную реализацию данного класса, для использования нужного нам поставщика от JCP

/**
 * 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.impl.ws.security.message;

import java.security.Provider;
import java.security.Security;

import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;

import org.apache.ws.security.WSSConfig;
import org.apache.ws.security.message.WSSecSignature;

/**
 * Переопределяем базовый объект WSSecSignature.
 * Цель - подставить DOM Provider "CryptoProXMLDSig" вместо базового "ApacheXMLDSig".
 * @author Aleksey Sushko
 *
 */
public class LocalWSSecSignature extends WSSecSignature {

 public LocalWSSecSignature() {
  super();
  init();
 }

 public LocalWSSecSignature(WSSConfig config) {
  super(config);
  init();
 }

 private void init() {
  Provider jceProvider = loadJceProvider();
  signatureFactory = XMLSignatureFactory.getInstance("DOM", jceProvider);
  keyInfoFactory = KeyInfoFactory.getInstance("DOM", jceProvider);
 }

 private Provider loadJceProvider() {
  Provider jceProvider = Security.getProvider("CryptoProXMLDSig");
  if(jceProvider == null) {
   jceProvider = new ru.CryptoPro.JCPxml.dsig.internal.dom.XMLDSigRI();
   Security.addProvider(jceProvider);
  }
  return jceProvider;
 }
}

Осталось найти, где идет создание базового WSSecSignature и сделать вызов нового LocalWSSecSignature. Этим узким местом является обработчик события подписания данных org.apache.ws.security.action.SignatureAction. Создаем ему собственную альтернативу

/**
 * 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.impl.xml.security.action;


import java.util.List;

import javax.security.auth.callback.CallbackHandler;

import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSEncryptionPart;
import org.apache.ws.security.WSPasswordCallback;
import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.action.Action;
import org.apache.ws.security.handler.RequestData;
import org.apache.ws.security.handler.WSHandler;
import org.apache.ws.security.message.WSSecSignature;
import org.apache.ws.security.util.WSSecurityUtil;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import org.company.soap.impl.ws.security.message.LocalWSSecSignature;

/**
 * Замена базового механизма подписания SOAP запроса.
 * Вместо создания WSSecSignature создается LocalWSSecSignature.
 * 
 * @see org.apache.ws.security.action.SignatureAction
 * @see org.apache.ws.security.message.WSSecSignature
 * @see org.company.soap.impl.ws.security.message.LocalWSSecSignature
 * @author Aleksey Sushko
 *
 */
public class LocalSignatureAction implements Action {
    public void execute(WSHandler handler, int actionToDo, Document doc, RequestData reqData)
            throws WSSecurityException {
        CallbackHandler callbackHandler = 
            handler.getPasswordCallbackHandler(reqData);
        WSPasswordCallback passwordCallback = 
            handler.getPasswordCB(reqData.getSignatureUser(), actionToDo, callbackHandler, reqData);
        WSSecSignature wsSign = loadWSSecSignature(reqData);

        if (reqData.getSigKeyId() != 0) {
            wsSign.setKeyIdentifierType(reqData.getSigKeyId());
        }
        if (reqData.getSigAlgorithm() != null) {
            wsSign.setSignatureAlgorithm(reqData.getSigAlgorithm());
        }
        if (reqData.getSigDigestAlgorithm() != null) {
            wsSign.setDigestAlgo(reqData.getSigDigestAlgorithm());
        }

        wsSign.setUserInfo(reqData.getSignatureUser(), passwordCallback.getPassword());
        wsSign.setUseSingleCertificate(reqData.isUseSingleCert());
        if (reqData.getSignatureParts().size() > 0) {
            wsSign.setParts(reqData.getSignatureParts());
        }
        
        if (passwordCallback.getKey() != null) {
            wsSign.setSecretKey(passwordCallback.getKey());
        }

        try {
            wsSign.prepare(doc, reqData.getSigCrypto(), reqData.getSecHeader());

            Element siblingElementToPrepend = null;
            for (WSEncryptionPart part : reqData.getSignatureParts()) {
                if ("STRTransform".equals(part.getName()) && part.getId() == null) {
                    part.setId(wsSign.getSecurityTokenReferenceURI());
                } else if (reqData.isAppendSignatureAfterTimestamp()
                        && WSConstants.WSU_NS.equals(part.getNamespace()) 
                        && "Timestamp".equals(part.getName())) {
                    List<Element> elements = 
                        WSSecurityUtil.findElements(
                            doc.getDocumentElement(), part.getName(), part.getNamespace()
                        );
                    if (elements != null && !elements.isEmpty()) {
                        Element timestampElement = elements.get(0);
                        Node child = timestampElement.getNextSibling();
                        while (child != null && child.getNodeType() != Node.ELEMENT_NODE) {
                            child = child.getNextSibling();
                        }
                        siblingElementToPrepend = (Element)child;
                    }
                }
            }

            List<javax.xml.crypto.dsig.Reference> referenceList = 
                wsSign.addReferencesToSign(reqData.getSignatureParts(), reqData.getSecHeader());

            if (reqData.isAppendSignatureAfterTimestamp() && siblingElementToPrepend == null) {
                wsSign.computeSignature(referenceList, false, null);
            } else {
                wsSign.computeSignature(referenceList, true, siblingElementToPrepend);
            }

            wsSign.prependBSTElementToHeader(reqData.getSecHeader());
            reqData.getSignatureValues().add(wsSign.getSignatureValue());
        } catch (WSSecurityException e) {
            throw new WSSecurityException("Error during Signature: ", e);
        }
    }

    protected WSSecSignature loadWSSecSignature(RequestData reqData) {
        WSSecSignature wsSign = new LocalWSSecSignature(reqData.getWssConfig());
        return wsSign;
    }
}

Настройка CXF

Ну вот, навалил кучу куда, а когда же дойдет дело до CXF? Как раз, настало время всё свести воедино. Конечно, можно не парится и показать кусок xml текста, в котором Spring поможет нам настроить CXF для работы с ЭЦП. Мне, по идейным соображениям, такой подход не интересен. Ни в JavaEE проектах, ни в OSGi проектах, я не использую Spring. Буду продолжать по старинки, Java like, так сказать.

В Apache CXF надо настроить систему перехватчиков событий работы с SOAP сообщениями.

/**
 * 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.smev.soap;

import java.util.HashMap;
import java.util.Map;

import org.apache.cxf.binding.soap.interceptor.SoapInterceptor;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.WSConstants;

import org.company.soap.impl.xml.security.action.LocalSignatureAction;

/**
 * Настройка системы перехвата и обработки сообщений
 * в реализации Apache CXF.
 * @author Aleksey Sushko
 */
public class SoapFeatures {

 public void initializeCleint(Object port) {
  Client client = ClientProxy.getClient(port);
  
  LoggingInInterceptor loggingInInterceptor = new LoggingInInterceptor();
  LoggingOutInterceptor loggingOutInterceptor = new LoggingOutInterceptor();
  
  client.getInInterceptors().add(loggingInInterceptor);
  client.getOutInterceptors().add(createSignatureOutInterceptor());
  client.getOutInterceptors().add(loggingOutInterceptor);
  client.getOutFaultInterceptors().add(loggingOutInterceptor);
 }

 /**
  * Параметр signatureUser определяет псевдоним ключа, используемого для подписи.
  * Параметр user нужен для KeystorePasswordCallback,
  * чтобы пользователь вернул пароль к закрытому ключу, указанному в signatureUser.
  * @return
  */
 private SoapInterceptor createSignatureOutInterceptor() {
  // определяем собственный класс системы подписывания
  Map<Integer, Class<?>> wssConnfigMap = new HashMap<Integer, Class<?>>();
  wssConnfigMap.put(Integer.valueOf(WSConstants.SIGN), LocalSignatureAction.class);

  Map<String, Object> params = new HashMap<String, Object>();
  params.put("wss4j.action.map", wssConnfigMap);
  params.put("action", "Signature");
  params.put("signaturePropFile", "crypto.properties");
  params.put("signatureKeyIdentifier", "DirectReference");
  params.put("user", "tester"); // KeystorePasswordCallback должен вернуть пароль к ключу пользователя
  params.put("signatureUser", "123456789012_12345678901234567890"); // SignatureAction берет это имя
  params.put("passwordCallbackClass", "org.company.soap.KeystorePasswordCallback");
  params.put("signatureDigestAlgorithm", "http://www.w3.org/2001/04/xmldsig-more#gostr3411");
  params.put("signatureAlgorithm", "http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411");
  
  // параметры для SMEV
  params.put("actor", "http://smev.gosuslugi.ru/actors/smev");
  params.put("mustUnderstand", "false");
  
  WSS4JOutInterceptor interceptor = new WSS4JOutInterceptor(params);
  
  return interceptor;
 }
}

Работа с сервисом

Из WSDL файла генерируем набор объектов
      <plugin>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-codegen-plugin</artifactId>
        <version>${cxf.version}</version>
        <executions>
          <execution>
            <id>generate-sources</id>
            <phase>generate-sources</phase>
            <configuration>
              <wsdlOptions>
                <wsdlOption>
                  <wsdl>${basedir}/src/main/resources/wsdl/SmevUnifoService.wsdl</wsdl>
                  <extraargs>
                    <extraarg>-b</extraarg>
                    <extraarg>${basedir}/src/main/xjb/binding.xjb</extraarg>
                  </extraargs>
                </wsdlOption>
              </wsdlOptions>
            </configuration>
            <goals>
              <goal>wsdl2java</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

Наконец, то я дошел до клиента SOAP сервиса

/**
 * 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.test;

import java.net.URL;
import java.util.Date;

import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Service;

import org.apache.cxf.configuration.security.ProxyAuthorizationPolicy;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.cxf.transports.http.configuration.ProxyServerType;
import org.junit.Test;

import org.company.soap.SoapFeatures;
import org.company.soap.impl.xml.security.XmlDSignTools;
import ru.gosuslugi.smev.rev111111.MessageDataType;
import ru.gosuslugi.smev.rev111111.MessageType;
import ru.roskazna.smevunifoservice.SmevUnifoService;
import ru.roskazna.smevunifoservice.UnifoTransferMsg;


public class TestSignSOAP {

 @Test
 public void test() throws Exception {
  XmlDSignTools.init();
  QName serviceName = new QName("http://roskazna.ru/SmevUnifoService/", "SmevUnifoService");  
  String serviceURL = "http://localhost:8080/test-smev/SmevUnifoService"; 
  
  URL wsdlURL = getClass().getClassLoader().getResource("wsdl/SmevUnifoService.wsdl");
  
  Service service = Service.create(wsdlURL, serviceName);
  SmevUnifoService port = service.getPort(SmevUnifoService.class);
  
  // перенаправляем на нужный сервер
  BindingProvider provider = (BindingProvider) port;
  provider.getRequestContext().put(
    BindingProvider.ENDPOINT_ADDRESS_PROPERTY, 
    serviceURL);
  
  // параметры прохождения корпоративного прокси сервера
  Client client = ClientProxy.getClient(port);
  HTTPConduit http = (HTTPConduit) client.getConduit();
  HTTPClientPolicy policy = new HTTPClientPolicy();
  policy.setAutoRedirect(true);
  policy.setAllowChunking(false);
  policy.setConnection(ConnectionType.KEEP_ALIVE);
  http.setClient(policy);
  
  
  // Настраиваем систему перехвата для подписания SOAP сообщения
  SoapFeatures features = new SoapFeatures();
  features.initializeCleint(port);
  
  MessageType message = createMessage();
  MessageDataType messageData = createMessageData();  
  UnifoTransferMsg request = new UnifoTransferMsg();
  request.setMessage(message);
  request.setMessageData(messageData);
  
  UnifoTransferMsg responce = port.unifoTransferMsg(request);
 }

 private MessageType createMessage() {
  MessageType message = new MessageType();
  return message;
 }
 
 private MessageDataType createMessageData() {
  MessageDataType message = new MessageDataType();
  return message;
 }

}

Надеюсь, демонстрация долгожданного SOAP клиента покажется читателям гораздо более приятным, нежели описание работы с SOAP Message API.