воскресенье, 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.

41 комментарий:

  1. Спасибо большое Алексей, где же Вы раньше то были :)
    Если не сложно можно как нибудь выложить исходники на все это дело.

    Спасибо.

    ОтветитьУдалить
  2. Я задумывался над тем, что было бы полезно выложить исходники на github. Хоть я и применяю у себя git, тем не менее, я надеюсь, что КриптоПро даст людям нормальную реализацию WSS4J стека и этот блог останется историей. Вторая причина - это правила заполнения отправляемых сообщений. Увы, я этого показать не могу.
    Исходники классов представлены полностью и их можно легко вырезать, открыв "исходный текст страницы". Я специально указал лицензию Apache, чтобы люди могли использовать их как в открытых, так и закрытых проектах.

    ОтветитьУдалить
    Ответы
    1. Алексей, спасибо большое! Лучшая статья и лучший пример, что я когда либо видел!

      Удалить
  3. Пытаюсь использовать Ваши наработки. Столкнулся с ошибкой :
    java.lang.ClassNotFoundException: org.apache.xml.security.c14n.InvalidCanonicalizerException

    ОтветитьУдалить
    Ответы
    1. Извините, что сразу не ответил.
      В файле проекта pom.xml имеются следующие версии программ:

      <properties>
      <project.build.sourceEncoding>UTF8</project.build.sourceEncoding>
      <cxf.version>2.7.3</cxf.version>
      <cryptopro-jcp.version>1.0.52</cryptopro-jcp.version>
      <slf.version>1.7.2</slf.version>
      <xmlsec.version>1.5.3</xmlsec.version>
      </properties>

      В список зависимостей надо указать
      <dependency>
      <groupId>org.apache.cxf</groupId>
      <artifactId>cxf-bundle</artifactId>
      <version>${cxf.version}</version>
      </dependency>
      <dependency>
      <groupId>org.apache.santuario</groupId>
      <artifactId>xmlsec</artifactId>
      <version>${xmlsec.version}</version>
      </dependency>
      <dependency>
      <groupId>org.bouncycastle</groupId>
      <artifactId>bcprov-jdk15on</artifactId>
      <version>1.48</version>
      </dependency>

      Удалить
  4. Не хватает класса org.company.soap.KeystorePasswordCallback

    ОтветитьУдалить
  5. Аналогично столкнулся с ошибкой :
    java.lang.ClassNotFoundException: org.apache.xml.security.c14n.InvalidCanonicalizerException
    Внес все в pom.xml не помогло.

    ОтветитьУдалить
    Ответы
    1. Сам себе отвечу http://www.cryptopro.ru/forum2/Default.aspx?g=posts&t=3919#post20733

      Удалить
    2. У КРИПТО-ПРО JCP 1.xx (на настоящий момент, осень 2013) сносит башню от Java7 (после кого-то апдейта) -- нужно ставить Java6 (официальная позиция КРИПТО-ПРО: залечено в JCP 2.xx, но она пока не сертифициирована, так что либо Java7+JCP2 или Java6+JCP1)
      иначе инсталлировать не удасться или если зашаманить, то получаем КУЧУ непонятных ошибок на пустом месте.

      Ошибка org.apache.xml.security.encryption.XMLEncryptionException: Unknown canonicalizer. No handler installed for URI http://santuario.apache.org/c14n/physical
      Original Exception was org.apache.xml.security.c14n.InvalidCanonicalizerException: Unknown canonicalizer. No handler installed for URI http://santuario.apache.org/c14n/physical

      Лечится заменой xmlsec-1.5.xx.jar на xmlsec-1.4.8.jar (http://santuario.apache.org/download.html => http://apache-mirror.rbc.ru/pub/apache/santuario/java-library/1_4_8/xml-security-bin-1_4_8.zip)

      Ошибка java.lang.UnsupportedOperationException
      at javax.crypto.CipherSpi.engineGetKeySize(DashoA13*..)
      at javax.crypto.Cipher.b(DashoA13*..)
      at javax.crypto.Cipher.a(DashoA13*..)
      at javax.crypto.Cipher.a(DashoA13*..)
      at javax.crypto.Cipher.a(DashoA13*..)
      at javax.crypto.Cipher.init(DashoA13*..)
      at javax.crypto.Cipher.init(DashoA13*..)
      at org.apache.xml.security.encryption.XMLCipher.encryptKey(Unknown Source)

      лечится заменой JDK/JRE\lib\security\local_policy.jar и US_export_policy.jar на файлы из jce_policy-6.zip (Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 6) http://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html

      Удалить
  6. Этот комментарий был удален автором.

    ОтветитьУдалить
  7. Этот комментарий был удален автором.

    ОтветитьУдалить
  8. Выдает ошибку: Endpoint {http://smev.gosuslugi.ru/SignatureTool/}SignatureToolPort does not contain operation meta data for: {http://roskazna.ru/SmevUnifoService/}UnifoTransferMsg
    Нашел несоответствие требованиям:
    2. Проставляется атрибут wsu:Id="body" элементу Body сообщения.
    В сообщении формируется wsu:Id="id-1"
    Кроме того нарушается требование: 6. Добавляется ссылка на данные для подписи и параметры каноникализации.
    Значение атрибута URI элемента ds:Reference должно соответствовать значению атрибута wsu:Id элемента soapenv:Body без лидирующего знака '#'.
    В сообщении

    ОтветитьУдалить
  9. Не совсем понятно, что у вас не получается. Для подписывания SOAP сообщения, надо правильно настроить систему подписывания отправляемого документа

    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";

    Map clientOutParams = new HashMap();
    clientOutParams.put(WSHandlerConstants.USER, "transmitter");

    // используемый алгоритм расчета хеш функции от содержимого
    clientOutParams.put(WSHandlerConstants.SIG_DIGEST_ALGO, CRYPTO_GOST_DIGEST);
    // используемый алгоритм подписывания
    clientOutParams.put(WSHandlerConstants.SIG_ALGO, CRYPTO_GOST_SIGN);

    // подписываем содержимое документа
    clientOutParams.put(WSHandlerConstants.SIGNATURE_PARTS,
    "{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body;");

    // вставляем сертификат ключа подписи
    clientOutParams.put(WSHandlerConstants.SIG_KEY_ID, "DirectReference");

    // параметры для SMEV
    clientOutParams.put("actor", "http://smev.gosuslugi.ru/actors/smev");
    clientOutParams.put("MustUnderstand", "false");

    WSS4JOutInterceptor wss4JOutInterceptor = new WSS4JOutInterceptor(clientOutParams);
    client.getOutInterceptors().add(wss4JOutInterceptor);

    Не забываем о том, что передаваемый XML документ может/должен содержать свою подпись.

    ОтветитьУдалить
    Ответы
    1. Тестовый сервер http://188.254.16.92:7777/gateway/services/SID0003038 возвращает ошибку. Подпись не проверял, в остальном запрос не соответствует двум требованиям из документации. ID body легко обходится установкой wssConfig.setIdAllocator().
      Второе требование: Значение атрибута URI элемента ds:Reference должно соответствовать значению атрибута wsu:Id элемента soapenv:Body без лидирующего знака '#'.
      Пока решение одно - ломать классы wss4j. Хотя возможно после реализации первого пункта заработает.
      Не могли бы Вы привести пример кода для подписи сущности (передаваемого XML)? Воспользовался кодом из примера крипто-про, но результат по одному пункту расходится с описанием правил подписи, возможно, будут проблемы.

      Удалить
    2. теперь soap:body id="body", но ошибка прежняя. буду ломать классы чтобы убрать # в

      Удалить
  10. Тот сервис мертв, правильный
    http://188.254.16.92:7777/gateway/services/SID0003218
    Остались два актуальных вопроса. Кодировка ответного SoapFault сообщения неправильная и на ней ломается unmarshaling. Сервис забраковал ключи, пока дальнейшую проверку произвести не могу. Второй вопрос по примеру подписи сущности (передаваемого XML).

    ОтветитьУдалить
  11. По спецификации XML Security, блок подписи содержит список подписываемых разделов XML документа. Знак # обозначает поиск элемента по DOM структуре документа. Можно явно указывать wsu:id, тогда символ # не нужен.


    Ответ SoapFault, указанного сервиса, идет в кодировке UTF-8. В заголовке HTTP ответа указана другая кодировка. Как результат, всё тело HTTP запроса читается в неправильной кодировке. Исправлять ошибку надо на стороне сервера.

    ОтветитьУдалить
  12. Не могу победить подпись сущностей. Выдает ошибку 22Подпись ЭП-СП под сущностью неверна. Пробовал подписывать 6-ю различными способами, проводить xml перед подписью через marshaling/unmarshaling . Последняя мысль сломать классы WSS4J и подписать уже часть body SOAP сообщения.

    ОтветитьУдалить
    Ответы
    1. Подписывание внутреннего XML документа выходит за рамки SOAP сообщения. Я написал небольшую статью на эту тему "Подписывание XML документов".
      Приглашаю ознакомится со статьей и обсудить.

      Удалить
  13. Удивлён, что кто-то осилил добить проблему и написать. Я почти год назад мучался с тем же. В итоге прикрутил Крипто Про с ГОСТовскими алгоритмами к более старому wss4j (и соответственно CXF).
    Хорошо, что я теперь ушёл от этого. Крипто Про для Java будет всегда головной болью для программиста. Оно явно делается "на отъе**сь", да и какие ещё варианты, если они монополисты. Какое бы г*вно Крипто Про-шники не произвели, программисты на госзаказах всё равно съедят, потому что сертификация и все дела.

    ОтветитьУдалить
  14. Я по началу делал под JCP. Потом, посмотрел Sun-овскую реализацию для MS CryptoAPI из OpenJDK. Полностью переписал, убрав все зависимости от алгоритмов. Алгоритмы запрашиваю у криптографического провайдера. В результате, у меня получился свой мост для CSP.
    Под этот мост модифицировал XML Digital Sign (XMLDSig). В этом блоге, я упоминал, как делается свой DOM provider.
    Результат - система работает на КриптоПро CSP под Windows и под Linux. Можно сэкономить большую кучу денег, отказавшись от JCP, под Intel I7.

    ОтветитьУдалить
    Ответы
    1. Круто! Ещё бы ваше решение можно было бы из maven утащить с исходниками.
      Мне уже не нужно, так другим бедолагам пригодится.

      Удалить
  15. может поделитесь лично или на guthub?
    не хочется проходить этот тернистый путь, если есть уже решение.

    ОтветитьУдалить
    Ответы
    1. До нового года постараюсь опубликовать на github Java+CSP и WS-Security. Сейчас по работе очень загружен.

      Удалить
    2. Ну я пока по примерам пытаюсь освоить JCP, какие то кусочки работают - например подписание SOAP из файла, wsdl-клиент.
      Нно wsdl клиент + подпись - пока не выходит каменный цветок.

      И попутно, не знаете, на сколько легально публиковать исходники по работе с CSP? С одной стороны то что у них в интернете опубликованы примеры никаких ограничений вроде нет. Но если посмотреть примеры в архивах то там чуть ли не "после прочтения сжечь".

      Удалить
    3. Почему работа с CSP должна быть закрыта? Там ведь используется MS CryptoAPI и Java JNI - мост между C и Java.
      Другое дело, что шифрование вызывает особые сложности. Там JAR модуль должен быть подписан ключём, зарегистрированным в Oracle. Но для подписания алгоритмы шифрования не используются. Поэтому, связка Java + MS CryptoAPI вполне нормальное явление.

      У меня такое мнение, что всех больше заботит использование контейнера подписи PKCS#7. По этой причине, в этой области так мало наработок с голым Java Crypto API.

      Удалить
    4. https://github.com/alexey-su/java-csp.git
      В windows 32 бит, я настраивал на MS Visual Studio 2008.
      В Linux x64 бит использовался штатный GNU C и дополнительные компоненты для разработчика криптографического провайдера.
      Нативная библиотека оказывается внутри jar архива.
      Провайдер, при первом запуске, распаковывает библиотеку и в дальнейшем ее использует.

      Удалить
    5. Возник вопрос - в CSPKeyStore.java приведены Windows-MY, Windows-ROOT, Windows-CA, Linux-AddressBook, FILE.
      Но они, по-видимому, не позволяют работать с контейнерами, которые лежат в линуксовом HDIMAGE? Или я ошибаюсь?

      Удалить
    6. Есть одна проблема. В CryptoPro JCP и в CryptoPro CSP есть два вида HDIMAGE.
      Это хорошо видно в JCP ControlPanel. Имеется два списка друг под другом. Верхний - список контейнеров закрытых ключей и сертификатов. Нижний - список файлов, хранилища сертификатов.

      CSPKeyStore не умеет читать контейнеры закрытых ключей и сертификатов. Скажу больше, файл списка сертификатов, "изготовлено в JCP", тоже не читается.
      Какие файлы тогда читает CSPKeyStore? Это файлы формата MS CryptoAPI. И открываются с помощью функции CertOpenStore.


      Вам, наверное, нужно такое решение:

      ks = CSPKeyStore.Builder.newInstance(type,
      Security.getProvider(provider),
      keyStoreLocation,
      (storepass != null ? storepass.toCharArray() : null)).getKeyStore();

      В проекте java-csp-wss4j можно найти класс LocalMerlin.
      Метод load реализует все механизмы чтения хранилища сертификатов.


      Каким способом можно работать с файлом сертификатов и иметь доступ к закрытому ключу?
      Для этих целей надо зарегистрировать сертификат и контейнер закрытого ключа в системном хранилище личных сертификатов (My / Windows-MY). В данном HDIMAGE хранилище, рядом с сертификатом хранится название контейнера закрытого ключа. Далее, средствами MS CryptoAPI копируем весь контейнер MY или его часть в новый контейнер (файл). В результате, в файле будет сертификат и название закрытого ключа.
      Можно попробовать сделать это средствами командной строки. С ходу, я не вспомню команды.

      CSPKeyStore содержит объекты CSPKeyStore.KeyEntry. Каждый объект состоит из названия ключа (псевдоним), дескриптора закрытого ключа (CSPKey privateKey) и массива списка отзыва. Первым в списке идет текущий сертификат. Если в хранилище нет сведений о закрытом ключе, параметр privateKey будет равен null.

      Удалить
  16. Для генерации стабов с wsdl у Вас используется файл файл биндинга binding.xjb. Можете приложить?

    ОтветитьУдалить
    Ответы
    1. В файле bind.xml нет ничего сложного.
      Используется преобразование в привычные объекты.

      <?xml version="1.0" encoding="UTF-8"?>
      <jaxb:bindings
      xmlns:xsd="http://www.w3.org/2001/XMLSchema"
      xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
      xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
      xsd:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd"
      jaxb:extensionBindingPrefixes="xjc"
      version="2.1">

      <jaxb:globalBindings>
      <jaxb:javaType
      name="java.util.Date"
      xmlType="xsd:dateTime"
      parseMethod="app.xml.bind.DatatypeConverter.parseDateTime"
      printMethod="app.xml.bind.DatatypeConverter.printDateTime" />

      <jaxb:javaType
      name="java.util.Date"
      xmlType="xsd:date"
      parseMethod="app.xml.bind.DatatypeConverter.parseDate"
      printMethod="app.xml.bind.DatatypeConverter.printDate" />

      <jaxb:javaType
      name="java.math.BigDecimal"
      xmlType="xsd:double" />
      </jaxb:globalBinding>
      </jaxb:bindings>

      Помог ли github?
      Там реализация ГОСТ подписей для CXF клиента и сервера.

      Удалить
  17. Этот комментарий был удален автором.

    ОтветитьУдалить
  18. Где ты живешь? я тебе поставлю ящик пива!

    ОтветитьУдалить
  19. Ошибки во время компиляции Compilation failure: XmlDSignTools.java:[54,37] error: constructor Algorithm in class Algorithm cannot be applied to given types;
    ....
    XmlDSignTools.java:[69,26] error: method register in class SignatureAlgorithm cannot be applied to given types;
    Слишком новый CXF? (2.7.11)

    ОтветитьУдалить
    Ответы
    1. Давайте лучше переходить на github. Там хотя бы можно ошибки править. Менять текст статьи в связи с изменением API внешних библиотек - неправильное занятие.

      https://github.com/alexey-su/java-csp.git

      Удалить
    2. Спасибо за быстрый ответ! Пример значительно разросся. Не со всем удалось разобраться пока, но у меня еще вопрос: возможно ли к этому прикрутить Security Policy, описываемую в wsdl, вместо interceptors ?

      Удалить
    3. Я не совсем ясно понимаю, о чем конкретно вы спрашиваете.
      Постараюсь ответить, так, как я понимаю.
      Interceptor - это технология внедрения дополнительных обработчиков. В контексте данной статьи и CXF, это создание и проверка электронной подписи.
      Security Policy - это описание правил авторизации клиента для сервера и подтверждение сервера, что ответ принадлежит конкретному серверу.
      Если в Security Policy указывается авторизация клиента с помощью сертификата, это означает, что клиентский SOAP (xml) запрос будет подписан и добавлены сведения о сертификате. Говоря проще, это и есть не что иное, как электронная подпись клиента.
      Далее, рассматриваем технологию со стороны сервера. Ему нужны дополнительные вставки (iterceptor), для проверки подлинности полученной подписи. Если сервер возвращает подписанный SOAP ответ, тогда и клиенту нужен данный набор вставок (interceptor), для проверки принадлежности ответа конкретному серверу, а не какому-то хакерскому серверу.

      Удалить
  20. Алексей, как будет выглядеть LocalSignatureAction, если мы подписываем сообщение, которое содержит в себе подпись в формате XAdES?

    ОтветитьУдалить
    Ответы
    1. Честное слово, у меня нет решения этого интересного вопроса.

      Удалить