Firmar WebServices Axis Con WSS4J y certificados X509

por jnavarretesanchez

El otro día en el trabajo tuve que llamar a un servicio web pero con la peculiaridad de que tenia que estar firmado con un certificado X.509.

Primero voy a poner las posibilidades que barajamos, probamos con varios clientes:

  1. Axis2 tiene un modulo llamado rampart que pinta bastante bien y tiene varios ejemplos.
  2. Apache CXF la evolución de Xfire también parece que se integra con Wss4j
  3. Axis ( lo que tuvo retuvo ) con Wss4j, este fue el primero que probé y también el primero que funciono, así que fue con el que me quede.

Si estas empezando con esto de los servicios web con java te recomiendo estos dos libros: uno sobre Axis y otro sobre Axis2 y CXF, por 15 euretes cada uno no esta mal y son de calidad.

A lo que íbamos, para crear el cliente Axis utilizamos el generador de eclipse

New >> Java Project , lo creamos y luego botón derecho en el proyecto New >> Other >> Web Service Client

  • En service definition metemos la url del wsdl,
  • Server puede quedar vacío
  • Web Service Runtime Axis
  • y el client project el que aparece por defecto ( nuestro nuevo proyecto ), siguiente, siguiente y acabamos.

Esto nos ha generado las clases del modelo y luego 5 clases que son:

  • NombreDelServicioWS.java
  • NombreDelServicioWSHttpBindingStub.java
  • NombreDelServicioWSLocator.java
  • NombreDelServicioWSPortType.java
  • NombreDelServicioWSPortTypeProxy.java

Solo nos interesa la clase NombreDelServicioWSHttpBindingStub.java, veremos que por cada método del servicio se a creado un método con ese nombre y parámetros necesarios. La parte que tenemos que buscar es en la cual se crea el Call ( sera algo tal que así )

org.apache.axis.client.Call _call = createCall();
_call.setOperation(_operations[0]);
_call.setUseSOAPAction(true);
_call.setSOAPActionURI("");
_call.setEncodingStyle(null);
_call.setProperty(org.apache.axis.client.Call.SEND_TYPE_ATTR, Boolean.FALSE);
_call.setProperty(org.apache.axis.AxisEngine.PROP_DOMULTIREFS, Boolean.FALSE);
_call.setSOAPVersion(org.apache.axis.soap.SOAPConstants.SOAP11_CONSTANTS);

Veremos que cuando termina de setear parámetros al Call llama al invoke. Pues antes de que llame al invoke nosotros tenemos que setear un RequestHandler, y para que?, pues para que firme la petición, que es para lo que te estas tragando el rollaco este.

Entonces la cosa quedaría tal que así:

org.apache.axis.client.Call _call = createCall();
	_call.setOperation(_operations[0]);
	_call.setUseSOAPAction(true);
	_call.setSOAPActionURI("");
	_call.setEncodingStyle(null);
	_call.setProperty(org.apache.axis.client.Call.SEND_TYPE_ATTR, Boolean.FALSE);
	_call.setProperty(org.apache.axis.AxisEngine.PROP_DOMULTIREFS, Boolean.FALSE);
	_call.setSOAPVersion(org.apache.axis.soap.SOAPConstants.SOAP11_CONSTANTS);
	_call.setOperationName(new javax.xml.namespace.QName(NAMESPACE, "NombreDelMetodo"));

	try {
		Properties configuration = new Properties();
		configuration.load(new FileInputStream(new File("securityConfiguration.properties")));
		// Creacion del manejador que securizara la peticion
		ClientHandler sender = new ClientHandler(configuration);
		_call.setClientHandlers(sender, null);
	} catch (Exception e) {
		e.printStackTrace();
	}

	setRequestHeaders(_call);
	setAttachments(_call);
	try {

		java.lang.Object _resp = _call.invoke(new java.lang.Object[] { numeroPedido, fechaDesde, fechaHasta, estado, cifInvocante, sociedades });

He visto algun sitio donde utilizaban el Handler org.apache.ws.axis.security.WSDoAllSender ( como por ejemplo uno de los libros que he puesto arriba), con el que luego tienes que setear varios parámetros al call, pero a mi no me ha funcionado con el certificado pfx.

Solo queda dejar el ClientHandler y el properties.

ClientHandler.java

package webservices;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Properties;
import javax.xml.soap.SOAPMessage;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.axis.AxisFault;
import org.apache.axis.Message;
import org.apache.axis.MessageContext;
import org.apache.axis.SOAPPart;
import org.apache.axis.handlers.BasicHandler;
import org.apache.axis.message.SOAPEnvelope;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.components.crypto.Crypto;
import org.apache.ws.security.components.crypto.CryptoFactory;
import org.apache.ws.security.message.WSSecHeader;
import org.apache.ws.security.message.WSSecSignature;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Clase encargada de securizar los mensajes SOAP de petición realizados desde
 * un cliente.
 *
 * @author SEPAOT
 *
 */
public class ClientHandler extends BasicHandler {

	private static final long serialVersionUID = 1L;

	// Opciones de seguridad

	// Localización del keystore con certificado y clave privada de usuario
	private String keystoreLocation = null;

	// Tipo de keystore
	private String keystoreType = null;

	// Clave del keystore
	private String keystorePassword = null;

	// Alias del certificado usado para firmar el tag soapBody de la petición y
	// que será alojado en el token BinarySecurityToken
	private String keystoreCertAlias = null;

	// Password del certificado usado para firmar el tag soapBody de la petición
	// y que será alojado en el token BinarySecurityToken
	private String keystoreCertPassword = null;

	/**
	 * Constructor que inicializa el atributo securityOption
	 *
	 * @param securityOption
	 *            opción de seguridad.
	 * @throws Exception
	 */
	public ClientHandler(Properties config) {
		if (config == null) {
			System.err.println("Fichero de configuracion de propiedades nulo");
			System.exit(-1);
		}
		try {

			keystoreLocation = config.getProperty("security.keystore.location");
			keystoreType = config.getProperty("security.keystore.type");
			keystorePassword = config.getProperty("security.keystore.password");
			keystoreCertAlias = config.getProperty("security.keystore.cert.alias");
			keystoreCertPassword = config.getProperty("security.keystore.cert.password");

		} catch (Exception e) {
			System.err.println("Error leyendo el fichero de configuración de securización");
			System.exit(-1);
		}
	}

	public void invoke(MessageContext msgContext) throws AxisFault {
		SOAPMessage secMsg = null;
		try {

			((SOAPPart) msgContext.getRequestMessage().getSOAPPart()).setCurrentMessage(this.createBinarySecurityToken(msgContext),
					SOAPPart.FORM_SOAPENVELOPE);

		} catch (Exception e) {
			System.err.println(e.getMessage());
			System.exit(-1);
		}
	}

	/**
	 * Securiza, mediante el tag BinarySecurityToken y firma una petición SOAP
	 * no securizada.
	 *
	 * @param soapEnvelopeRequest
	 *            Documento xml que representa la petición SOAP sin securizar.
	 * @return Un mensaje SOAP que contiene la petición SOAP de entrada
	 *         securizada mediante el tag BinarySecurityToken.
	 */
	private SOAPEnvelope createBinarySecurityToken(MessageContext msgContext) {
		ByteArrayOutputStream baos;
		Crypto crypto;
		Document secSOAPReqDoc,soapEnvelopeRequest;
		DOMSource source;
		Element element;
		StreamResult streamResult;
		String secSOAPReq;
		WSSecSignature wsSecSignature;
		WSSecHeader wsSecHeader;
		SOAPMessage msg;

		try {

			// Obtención del documento XML que representa la petición SOAP
			msg = msgContext.getCurrentMessage();
			soapEnvelopeRequest = ((org.apache.axis.message.SOAPEnvelope) msg.getSOAPPart().getEnvelope()).getAsDocument();

			// Inserción del tag wsse:Security y BinarySecurityToken
			wsSecHeader = new WSSecHeader(null, false);
			wsSecSignature = new WSSecSignature();
			crypto = CryptoFactory.getInstance("org.apache.ws.security.components.crypto.Merlin", this.initializateCryptoProperties());
			// Indicación para que inserte el tag BinarySecurityToken
			wsSecSignature.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE);
			// wsSecSignature.setKeyIdentifierType(WSConstants.ISSUER_SERIAL);
			wsSecSignature.setUserInfo(this.keystoreCertAlias, this.keystoreCertPassword);
			wsSecHeader.insertSecurityHeader(soapEnvelopeRequest);
			wsSecSignature.prepare(soapEnvelopeRequest, crypto, wsSecHeader);
			// Modificación y firma de la petición
			secSOAPReqDoc = wsSecSignature.build(soapEnvelopeRequest, crypto, wsSecHeader);
			element = secSOAPReqDoc.getDocumentElement();
			// Transformación del elemento DOM a String
			source = new DOMSource(element);
			baos = new ByteArrayOutputStream();
			streamResult = new StreamResult(baos);
			TransformerFactory.newInstance().newTransformer().transform(source, streamResult);
			secSOAPReq = new String(baos.toByteArray());

			// Creación de un nuevo mensaje SOAP a partir del mensaje SOAP
			// securizado formado
			Message axisMessage = getAxisMessage(secSOAPReq,msgContext);
	        return axisMessage.getSOAPEnvelope();

		} catch (Exception e) {
			System.err.println(e.getMessage());
			System.exit(-1);
			return null;

		}
	}

	/**
	 * Establece el conjunto de propiedades con el que será inicializado el
	 * gestor criptográfico de WSS4J.
	 *
	 * @return Devuelve el conjunto de propiedades con el que será inicializado
	 *         el gestor criptográfico de WSS4J.
	 */
	private Properties initializateCryptoProperties() {
		Properties res = new Properties();
		res.setProperty("org.apache.ws.security.crypto.provider", "org.apache.ws.security.components.crypto.Merlin");
		res.setProperty("org.apache.ws.security.crypto.merlin.keystore.type", this.keystoreType);
		res.setProperty("org.apache.ws.security.crypto.merlin.keystore.password", this.keystorePassword);
		res.setProperty("org.apache.ws.security.crypto.merlin.keystore.alias", this.keystoreCertAlias);
		res.setProperty("org.apache.ws.security.crypto.merlin.alias.password", this.keystoreCertPassword);
		res.setProperty("org.apache.ws.security.crypto.merlin.file", this.keystoreLocation);
		return res;
	}

	/**
	 * Creates and returns an Axis message from a SOAP envelope string.
	 *
	 * @param unsignedEnvelope
	 *            a string containing a SOAP envelope
	 * @return <code>Message</code> the Axis message
	 */
	private Message getAxisMessage(String unsignedEnvelope, MessageContext msgContext) {
		InputStream inStream = new ByteArrayInputStream(unsignedEnvelope.getBytes());
		Message axisMessage = new Message(inStream);
		axisMessage.setMessageContext(msgContext);
		return axisMessage;
	}
}

securityConfiguration.properties

##############################################################
#                          SECURIZACIÓN DE PETICIÓN SOAP DEL CLIENTE         #
##############################################################

# - location:       Ruta al almacén que contiene el certificado y la clave privada con la que firmar la petición WS
# - type:             Tipo de almacén (PKCS12, JKS)
# - password:      password del Almacén
# - cert alias: Alias del certificado del usuario que está dentro del almacén
# - cert password: Password de la clave privada correspondiente al certificado anterior

security.keystore.location=/home/sorack/certificado.pfx
security.keystore.type=PKCS12
security.keystore.password=password
security.keystore.cert.alias={8aeff3e45fa-65543c-4636-8ff6-5dd8453e4a0f1b}
security.keystore.cert.password=password

La clase ClientHanlder utiliza trozos de código que he vi en este manual

Por ultimo comentar que Wss4j no admite namespaces relativos, el error que da es algo como » element x has a relative namespace»