View Javadoc

1   /**
2    * LICENCIA LGPL:
3    * 
4    * Esta librería es Software Libre; Usted puede redistribuirla y/o modificarla
5    * bajo los términos de la GNU Lesser General Public License (LGPL) tal y como 
6    * ha sido publicada por la Free Software Foundation; o bien la versión 2.1 de 
7    * la Licencia, o (a su elección) cualquier versión posterior.
8    * 
9    * Esta librería se distribuye con la esperanza de que sea útil, pero SIN 
10   * NINGUNA GARANTÍA; tampoco las implícitas garantías de MERCANTILIDAD o 
11   * ADECUACIÓN A UN PROPÓSITO PARTICULAR. Consulte la GNU Lesser General Public 
12   * License (LGPL) para más detalles
13   * 
14   * Usted debe recibir una copia de la GNU Lesser General Public License (LGPL) 
15   * junto con esta librería; si no es así, escriba a la Free Software Foundation 
16   * Inc. 51 Franklin Street, 5º Piso, Boston, MA 02110-1301, USA o consulte
17   * <http://www.gnu.org/licenses/>.
18   *
19   * Copyright 2011 Agencia de Tecnología y Certificación Electrónica
20   */
21  package es.accv.arangi.certificate.validation;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.StringReader;
26  import java.net.MalformedURLException;
27  import java.net.URL;
28  import java.text.SimpleDateFormat;
29  import java.util.Arrays;
30  import java.util.Date;
31  import java.util.HashMap;
32  import java.util.Map;
33  
34  import javax.xml.parsers.DocumentBuilder;
35  import javax.xml.parsers.DocumentBuilderFactory;
36  import javax.xml.xpath.XPath;
37  import javax.xml.xpath.XPathConstants;
38  import javax.xml.xpath.XPathExpression;
39  import javax.xml.xpath.XPathFactory;
40  
41  import org.apache.log4j.Logger;
42  import org.bouncycastle.asn1.ASN1Encodable;
43  import org.bouncycastle.asn1.ASN1Enumerated;
44  import org.bouncycastle.asn1.ASN1ObjectIdentifier;
45  import org.bouncycastle.asn1.ASN1Sequence;
46  import org.bouncycastle.asn1.DEROctetString;
47  import org.bouncycastle.asn1.DERSequence;
48  import org.bouncycastle.asn1.DERTaggedObject;
49  import org.w3c.dom.Document;
50  import org.w3c.dom.Node;
51  import org.w3c.dom.NodeList;
52  import org.xml.sax.InputSource;
53  
54  import es.accv.arangi.base.certificate.Certificate;
55  import es.accv.arangi.base.certificate.validation.OCSPResponse;
56  import es.accv.arangi.base.certificate.validation.CertificateValidationService;
57  import es.accv.arangi.base.certificate.validation.CertificateValidationServiceResult;
58  import es.accv.arangi.base.exception.certificate.NormalizeCertificateException;
59  import es.accv.arangi.base.exception.certificate.validation.ServiceException;
60  import es.accv.arangi.base.exception.certificate.validation.ServiceNotFoundException;
61  import es.accv.arangi.base.util.Util;
62  import es.accv.arangi.base.util.validation.ValidationResult;
63  
64  /**
65   * Clase que implementa la validación de certificados mediante llamadas
66   * a los servicios web de &#64;Firma (puede verse la documentación en la zona
67   * segura de la página web de la ACCV: 
68   * <a href="https://www.accv.es:8445/secure_area/descargas/descargas/docsatFirma/bienvenida.htm" target="zonaSegura">
69   * https://www.accv.es:8445/secure_area/descargas/descargas/docsatFirma/bienvenida.htm</a>).
70   * 
71   * @author <a href="mailto:jgutierrez@accv.es">José Manuel Gutiérrez Núñez</a>
72   *
73   */
74  public class AFirmaCertificateValidationService implements CertificateValidationService {
75  
76  	/**
77  	 * URL del servicio web de &#64;firma en explotación
78  	 */
79  	public static final String PRODUCTION_URL = "http://afirma.accv.es/afirmaws/services/ValidarCertificado";
80  	
81  	/**
82  	 * URL del servicio web de &#64;firma en test
83  	 */
84  	public static final String TEST_URL = "http://preafirma.accv.es/afirmaws/services/ValidarCertificado";
85  	
86  	/*
87  	 * Nombre de la plantilla para generar llamadas sin securizar
88  	 */
89  	private static final String TEMPLATE_WITHOUT_SECURITY = "es/accv/arangi/base/template/arangi-afirma_validate_template.xml";
90  
91  	/*
92  	 * Nombre de la plantilla para generar llamadas mediante usuario y contraseña
93  	 */
94  	private static final String TEMPLATE_USER_PASSWORD = "es/accv/arangi/base/template/arangi-afirma_validate_secure_template.xml";
95  
96  	/*
97  	 * Código de &#64;Firma para certificado válido
98  	 */
99  	private static final int AFIRMA_RESULT_OK		= 0;
100 	
101 	/*
102 	 * Código de &#64;Firma para resultado incorrecto
103 	 */
104 	private static final int AFIRMA_RESULT_NOK		= 1;
105 	
106 	/*
107 	 * Código de &#64;Firma para cadena de validación incorrecta
108 	 */
109 	private static final int AFIRMA_RESULT_CHAIN_VALIDATION_INVALID	= 2;
110 	
111 	/*
112 	 * Código de &#64;Firma para certificado revocado
113 	 */
114 	private static final int AFIRMA_RESULT_REVOKED		= 3;
115 	
116 	/*
117 	 * Código de &#64;Firma para error obteniendo el resultado
118 	 */
119 	private static final int AFIRMA_RESULT_ERROR		= 4;
120 	
121 	/**
122 	 * Formateador de fechas que vienen de &#64;Firma
123 	 */
124 	public static final SimpleDateFormat AFIRMA_DATE_FORMAT	= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
125 	
126 	/*
127 	 * Logger de la clase
128 	 */
129 	Logger logger = Logger.getLogger(AFirmaCertificateValidationService.class);
130 	
131 	/*
132 	 * URL de acceso a los servicios web de &#64;Firma
133 	 */
134 	private URL url;
135 	
136 	/*
137 	 * Identificador de la aplicación en &#64;Firma
138 	 */
139 	private String idAplicacion;
140 	
141 	/*
142 	 * Usuario 
143 	 */
144 	private String user;
145 	
146 	/*
147 	 * Contraseña
148 	 */
149 	private String password;
150 	
151 	//-- Constructores	
152 	
153 	/**
154 	 * Constructor por defecto: si se usa este constructor será necesario inicializar
155 	 * el objeto.
156 	 */
157 	public AFirmaCertificateValidationService() {
158 		super();
159 	}
160 	
161 	/**
162 	 * Constructor en el que pasar la información necesaria para crear 
163 	 * este objeto.
164 	 * 
165 	 * @param url URL al servico web de &#64;Firma. Los posibles valores se pueden
166 	 * 	encontrar en los campos estáticos de esta clase PRODUCTION_URL y
167 	 *  TEST_URL.
168 	 * @param idAplicacion ID de su aplicación. Este valor se le entregó en
169 	 * 	el momento en que su aplicación fue dada de alta en la plataforma de
170 	 * 	.
171 	 * @param user Nombre de usuario para el caso en que se deba realizar la
172 	 * 	llamada securizada mediante usuario y contraseña.
173 	 * @param password Contraseña para el caso en que se deba realizar la
174 	 * 	llamada securizada mediante usuario y contraseña.
175 	 */
176 	public AFirmaCertificateValidationService(URL url, String idAplicacion, String user,
177 			String password) {
178 		super();
179 		this.url = url;
180 		this.idAplicacion = idAplicacion;
181 		this.user = user;
182 		this.password = password;
183 	}
184 
185 	/**
186 	 * Inicializa el objeto
187 	 * 
188 	 * @param url URL al servico web de &#64;Firma. Los posibles valores se pueden
189 	 * 	encontrar en los campos estáticos de esta clase PRODUCTION_URL y
190 	 *  TEST_URL.
191 	 * @param idAplicacion ID de su aplicación. Este valor se le entregó en
192 	 * 	el momento en que su aplicación fue dada de alta en la plataforma de
193 	 * 	&#64;Firma.
194 	 * @param user Nombre de usuario para el caso en que se deba realizar la
195 	 * 	llamada securizada mediante usuario y contraseña.
196 	 * @param password Contraseña para el caso en que se deba realizar la
197 	 * 	llamada securizada mediante usuario y contraseña.
198 	 */
199 	public void initialize (URL url, String idAplicacion, String user, String password) {
200 		this.url = url;
201 		this.idAplicacion = idAplicacion;
202 		this.user = user;
203 		this.password = password;
204 	}
205 
206 	/**
207 	 * Valida un certificado mediante una llamada a un servicio externo.
208 	 * 
209 	 * @param certificate Certificado a validar
210 	 * @param extraParams Parámetros extra por si fueran necesarios para 
211 	 * 	realizar la validación
212 	 * @return Objeto con el resultado y, si el servicio web lo permite, los
213 	 * 	campos más significativos del certificado.
214 	 * @throws ServiceNotFoundException El servicio no se encuentra disponible
215 	 * @throws ServiceException La llamada al servicio devuelve un error
216 	 */
217 	public CertificateValidationServiceResult validate(Certificate certificate, 
218 			Map<String, Object> extraParams) throws ServiceNotFoundException, ServiceException {
219 		
220 		logger.debug("[AFirmaCertificateValidationService.validate]::Entrada::" + Arrays.asList(new Object[] { certificate, extraParams }));
221 		
222 		//-- Obtener el template
223 		InputStream isTemplate;
224 		if (this.user != null && this.password != null) {
225 			isTemplate = new Util().getClass().getClassLoader().getResourceAsStream(TEMPLATE_USER_PASSWORD);
226 		} else {
227 			isTemplate = new Util().getClass().getClassLoader().getResourceAsStream(TEMPLATE_WITHOUT_SECURITY);
228 		}
229 
230 		//-- Obtener los parámetros
231 		HashMap<String, String> parameters = new HashMap<String, String>();
232 		if (this.user != null && this.password != null) {
233 			parameters.put("user", this.user);
234 			parameters.put("password", this.password);
235 		} 
236 		try {
237 			parameters.put("certificate", Util.encodeBase64(certificate.toDER()));
238 		} catch (NormalizeCertificateException e) {
239 			//-- El certificado ya se normalizó al entrar, no se dará el error
240 			logger.info ("[AFirmaCertificateValidationService.validate]", e);
241 		}
242 		parameters.put("idAplicacion", this.idAplicacion);
243 
244 		//-- Obtener el mensaje
245 		String message;
246 		try {
247 			message = Util.fillTemplate(isTemplate, parameters);
248 		} catch (IOException e) {
249 			logger.info ("[AFirmaCertificateValidationService.validate]::Error construyendo el mensaje", e);
250 			throw new ServiceException("Error construyendo el mensaje", e);
251 		}
252 		logger.debug("[AFirmaCertificateValidationService.validate]::Se ha obtenido el mensaje a enviar a @Firma: " + message);
253 		
254 		//-- Enviar el mensaje
255 		StringBuffer respuesta = Util.sendPost(message, url);
256 		logger.debug("[AFirmaCertificateValidationService.validate]::Se ha obtenido la respuesta de @Firma: " + respuesta);
257 
258 		//-- Variables para obtener el resultado
259 		Map<String, Object> campos = new HashMap<String, Object>();
260 		int resultado = ValidationResult.RESULT_CERTIFICATE_CANNOT_BE_VALIDATED;
261 		Date fechaRevocacion = null;
262 		int motivoRevocacion = -1;
263 		OCSPResponse respuestaOCSP = null;
264 		
265 		//-- Comprobar si es mensaje de error
266 		if (respuesta.indexOf("codigoError") > -1) {
267 			//-- Comprobar si es porque no se trata el certificado (COD_066) o no se trata para
268 			//-- el ID de aplicación (COD_063) o el tipo de certificado se encuentra deshabilitado(COD_064)
269 			if (respuesta.indexOf("COD_066") > -1 || respuesta.indexOf("COD_063") > -1 || respuesta.indexOf("COD_064") > -1) {
270 				logger.debug("[AFirmaCertificateValidationService.validate]::El certificado es desconocido");
271 				resultado = ValidationResult.RESULT_CERTIFICATE_UNKNOWN;
272 				return new CertificateValidationServiceResult(resultado, campos);
273 			} else {
274 				logger.info("[AFirmaCertificateValidationService.validate]::La respuesta de @Firma es de&#64;Firma");
275 				throw new ServiceException(respuesta.substring(respuesta.indexOf("codigoError&gt;") + 15, respuesta.indexOf("&lt;/codigoError")) + " - " +
276 						respuesta.substring(respuesta.indexOf("descripcion&gt;") + 15, respuesta.indexOf("&lt;/descripcion")));
277 			}
278 		}
279 		
280 		//-- Mensaje OK, extraer la información
281 		logger.debug("[AFirmaCertificateValidationService.validate]::La respuesta de @Firma no es de error");
282 		String xml = respuesta.substring(respuesta.indexOf("&lt;?xml"), respuesta.indexOf("</ValidarCertificadoReturn>"));
283 		xml = xml.replaceAll("\\&lt;", "<").replaceAll("\\&gt;", ">");
284 		
285 		Document doc;
286 		XPath xpath;
287 		try {
288 			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
289 			dbf.setNamespaceAware(true);
290 			DocumentBuilder builder = dbf.newDocumentBuilder();
291 			doc = builder.parse(new InputSource(new StringReader(xml)));
292 			XPathFactory factory = XPathFactory.newInstance();
293 			xpath = factory.newXPath();
294 		} catch (Exception e) {
295 			logger.info("[AFirmaCertificateValidationService.validate]::Error obteniendo el documento DOM o el XPath", e);
296 			throw new ServiceException("Error obteniendo el documento DOM o el XPath", e);
297 		}
298 
299 		//-- Obtener el resultado
300 		try {
301 			logger.debug("[AFirmaCertificateValidationService.validate]::Obteniendo el resultado");
302 			XPathExpression expr = xpath.compile("//*[local-name()='ResultadoProcesamiento']/*[local-name()='ResultadoValidacion']/*[local-name()='resultado']");
303 			Node resultNode = (Node) expr.evaluate (doc, XPathConstants.NODE);
304 			int aFirmaResultado = Integer.parseInt(resultNode.getTextContent());
305 			switch (aFirmaResultado) {
306 				case AFIRMA_RESULT_OK:
307 					resultado = ValidationResult.RESULT_VALID;
308 					break;
309 				case AFIRMA_RESULT_NOK:
310 					resultado = ValidationResult.RESULT_CERTIFICATE_CANNOT_BE_VALIDATED;
311 					break;
312 				case AFIRMA_RESULT_CHAIN_VALIDATION_INVALID:
313 					resultado = ValidationResult.RESULT_CERTIFICATE_CHAIN_VALIDATION_INVALID;
314 					break;
315 				case AFIRMA_RESULT_REVOKED:
316 					resultado = ValidationResult.RESULT_CERTIFICATE_REVOKED;
317 					expr = xpath.compile("//*[local-name()='InfoMetodoVerificacion']/*[local-name()='fechaRevocacion']");
318 					resultNode = (Node) expr.evaluate (doc, XPathConstants.NODE);
319 					String sFecha = resultNode.getTextContent().substring(0,10) + " " + resultNode.getTextContent().substring(15,23);
320 					fechaRevocacion = AFIRMA_DATE_FORMAT.parse(sFecha);
321 					expr = xpath.compile("//*[local-name()='InfoMetodoVerificacion']/*[local-name()='motivo']");
322 					resultNode = (Node) expr.evaluate (doc, XPathConstants.NODE);
323 					motivoRevocacion = Integer.parseInt(resultNode.getTextContent());
324 					break;
325 				case AFIRMA_RESULT_ERROR:
326 					resultado = ValidationResult.RESULT_CERTIFICATE_CANNOT_BE_VALIDATED;
327 					break;
328 			}
329 			logger.debug("[AFirmaCertificateValidationService.validate]::El resultado es " + resultado);
330 		} catch (Exception e) {
331 			logger.info("[AFirmaCertificateValidationService.validate]::Error obteniendo el resultado del XML de respuesta", e);
332 			throw new ServiceException("Error obteniendo el resultado del XML de respuesta", e);
333 		}
334 		
335 		//-- Obtener los campos
336 		try {
337 			XPathExpression expr = xpath.compile("//*[local-name()='ResultadoProcesamiento']/*[local-name()='InfoCertificado']/*[local-name()='Campo']");
338 			NodeList fieldNodes = (NodeList) expr.evaluate (doc, XPathConstants.NODESET);
339 			for (int i=0;i<fieldNodes.getLength();i++) {
340 				Node fieldNode = fieldNodes.item(i);
341 				String nombre = fieldNode.getChildNodes().item(0).getTextContent();
342 				String valor = fieldNode.getChildNodes().item(1).getTextContent();
343 				campos.put(nombre, valor);
344 			}
345 			logger.debug("[AFirmaCertificateValidationService.validate]::Se han obtenido " + campos.size() + " campos");
346 
347 		} catch (Exception e) {
348 			logger.info("[AFirmaCertificateValidationService.validate]::Error obteniendo los campos del XML de respuesta", e);
349 		}
350 		
351 		//-- Obtener la respuesta OCSP
352 		try {
353 			XPathExpression expr = xpath.compile("//*[local-name()='ResultadoProcesamiento']/*[local-name()='ResultadoValidacion']/*[local-name()='ValidacionEstado']/*[local-name()='InfoMetodoVerificacion']/*[local-name()='tokenOCSP']");
354 			Node node = (Node) expr.evaluate (doc, XPathConstants.NODE);
355 			if (node != null) {
356 				logger.debug("[AFirmaCertificateValidationService.validate]::Existe la respuesta OCSP dentro de la respuesta de @Firma");
357 				try {
358 					respuestaOCSP = new OCSPResponse(Util.decodeBase64(node.getTextContent()));
359 				} catch (Exception e) {
360 					//-- Tal vez está devolviendo el basicOcspResponse
361 					ASN1Sequence responseBytes = new DERSequence(new ASN1Encodable[] { new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1.1"), new DEROctetString(Util.decodeBase64(node.getTextContent())) });
362 					DERTaggedObject taggedObject = new DERTaggedObject(true, 0, responseBytes);
363 					ASN1Sequence sequence = new DERSequence(new ASN1Encodable[] { new ASN1Enumerated(0), taggedObject });
364 					respuestaOCSP = new OCSPResponse(sequence.getEncoded());
365 				}
366 			}
367 
368 		} catch (Exception e) {
369 			logger.info("[AFirmaCertificateValidationService.validate]::Error obteniendo la respuesta OCSP", e);
370 		}
371 
372 		//-- Devolver resultado
373 		CertificateValidationServiceResult certResult = new CertificateValidationServiceResult(resultado, campos);
374 		if (fechaRevocacion != null) {
375 			certResult.setRevocationDate(fechaRevocacion);
376 			certResult.setRevocationReason(motivoRevocacion);
377 		}
378 		if (respuestaOCSP != null) {
379 			certResult.setOcspResponse(respuestaOCSP);
380 		}
381 		
382 		return certResult;
383 	}
384 	
385 	/**
386 	 * Obtiene la URL de los servicios web de &#64;Firma en producción
387 	 * 
388 	 * @return URL
389 	 */
390 	public static URL getProductionURL () {
391 		try {
392 			return new URL(PRODUCTION_URL);
393 		} catch (MalformedURLException e) {
394 			// Siempore estará bien formada
395 			return null;
396 		}
397 	}
398 
399 	/**
400 	 * Obtiene la URL de los servicios web de &#64;Firma en test
401 	 * 
402 	 * @return URL
403 	 */
404 	public static URL getTestURL () {
405 		try {
406 			return new URL(TEST_URL);
407 		} catch (MalformedURLException e) {
408 			// Siempore estará bien formada
409 			return null;
410 		}
411 	}
412 
413 
414 }