Java tutorial
/* Copyright (C) 2011 [Gobierno de Espana] * This file is part of "Cliente @Firma". * "Cliente @Firma" is free software; you can redistribute it and/or modify it under the terms of: * the GNU General Public License as published by the Free Software Foundation; * either version 2 of the License, or (at your option) any later version. * or The European Software License; either version 1.1 or (at your option) any later version. * Date: 11/01/11 * You may contact the copyright holder at: soporte.afirma5@mpt.es */ package es.gob.afirma.signers.pades; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.GregorianCalendar; import java.util.Properties; import com.lowagie.text.DocumentException; import com.lowagie.text.pdf.PdfDictionary; import com.lowagie.text.pdf.PdfName; import com.lowagie.text.pdf.PdfSignatureAppearance; import com.lowagie.text.pdf.PdfString; import es.gob.afirma.core.AOException; import es.gob.afirma.core.misc.AOUtil; import es.gob.afirma.core.signers.AOSignConstants; import es.gob.afirma.core.signers.AdESPolicy; import es.gob.afirma.signers.cades.CAdESSignerMetadataHelper; import es.gob.afirma.signers.cades.CAdESTriPhaseSigner; import es.gob.afirma.signers.cades.CommitmentTypeIndicationsHelper; import es.gob.afirma.signers.tsp.pkcs7.CMSTimestamper; import es.gob.afirma.signers.tsp.pkcs7.TsaParams; /** Clase para la firma electrónica en tres fases de ficheros Adobe PDF en formato PAdES. * <p>No firma PDF cifrados.</p> * <p>Necesita iText 2.1.7 con modificaciones específicas.</p> * <p>Esta clase no interacciona directamente en ningún momento con el usuario ni usa interfaces gráficos.</p> * <p>La firma electrónica en tres fases está pensada para entornos donde la clave privada reside * en un sistema con al menos alguna de las siguientes restricciones:</p> * <ul> * <li> * El sistema no es compatible con el Cliente @firma. En este caso, dado que el 95% del código se * ejecuta en un sistema externo, solo es necesario portar el 5% restante. * </li> * <li> * El sistema tiene unas capacidades muy limitadas en cuanto a proceso computacional, memoria o comunicaciones por * red. En este caso, el sistema solo realiza una operación criptográfica, una firma PKCS#1, * mucho menos demandante de potencia de proceso que una firma completa PAdES, y, adicionalmente, no trata el * documento a firmar completo, sino úicamente una prqueña cantidad de datos resultante de un * pre-proceso (la pre-firma) realizado por el sistema externo, lo que resulta en un enorme decremento en las necesidades * de memoria y transmisión de datos. * </li> * <li> * Por motivos de seguridad, el documento a firmar no puede salir de un sistema externo. Como se ha descrito en el punto * anterior, en este caso el documento jamás sale del sistema externo, sino que se transfiere únicamente * el resultado de la pre-firma, desde la cual es imposible reconstruir el documento original. * </li> * </ul> * <p> * Estos condicionantes convierten la firma trifásica en una opción perfectamente adaptada a los * dispositivos móviles, donde se dan tanto la heterogeneidad de sistemas operativos (Apple iOS, Google * Android, RIM BlackBerry, Microsoft Windows Phone, etc.) y las limitaciones en potencia de proceso, memoria * y comunicaciones; en estas últimas hay que tener en cuenta el coste, especialmente si estamos haciendo * uso de una red de otro operador en itinerancia (<i>roaming</i>). * </p> * <p> * El funcionamiento típico de una firma trifásica en la que intervienen un disposotivo móvil, * un servidor Web (que hace la pre-firma y la post-firma) y un servidor documental podría ser el siguiente: * </p> * <p><b>Pre-firma:</b></p> * <p style="text-align: center;"><img src="doc-files/PAdESTriPhaseSigner-1.png" alt="Pre-firma"></p> * <ul> * <li>El dispositivo móvil solicita una pre-firma al servidor Web indicando un identificador de documento.</li> * <li>El servidor Web solicita el documento a servidor documental.</li> * <li> * El servidor documental entrega el documento al servidor Web.<br>Es importante recalcar que el servidor * documental no necesita almacenar ningún dato de sesión y que este no está expuesto a Internet * de forma directa en ningún momento. * </li> * <li> * El servidor Web calcula la pre-firma, entregando el resultado (muy pequeño en tamaño) al dispositivo.<br> * Es importante recalcar que el servidor Web no necesita almacenar ningún dato de sesión ni * exponer los documentos directamente al dispositivo. * </li> * </ul> * <p><b>Firma:</b></p> * <p style="text-align: center;"><img src="doc-files/PAdESTriPhaseSigner-2.png" alt="Firma"></p> * <ul> * <li> * El dispositivo móvil realiza, de forma completamente aislada una firma electrónica * simple (computacionalmente ligera) de los datos de la pre-firma. La clave privada del usuario nunca sale * del dispositivo y no se expone externamente en ningún momento. * </li> * </ul> * <p><b>Post-firma:</b></p> * <p style="text-align: center;"><img src="doc-files/PAdESTriPhaseSigner-3.png" alt="Post-firma"></p> * <ul> * <li> * El dispositivo móvil solicita una post-firma al servidor Web indicando un identificador de * documento y proporcionando el resultado de su pre-firma firmada. * </li> * <li>El servidor Web solicita el documento a servidor documental.</li> * <li>El servidor documental entrega el documento al servidor Web.</li> * <li> * El servidor Web calcula la post-firma y compone el documento final firmado, entregando el resultado * al servidor documental para su almacén. * </li> * <li>El servidor documental almacena el nuevo documento y devuelve un identificador al servidor Web.</li> * <li> * El servidor Web comunica al dispositivo el éxito de la operación y el identificador del fichero * ya firmado y almacenado. * </li> * </ul> * <p> * Es conveniente tener en cuenta al usar firmas trifásicas que es necesario disponer de un mecanismo * para que el usuario pueda ver en todo momento los documentos que está firmando (una copia que refleje * con fidelidad el contenido firmado puede ser suficiente) para evitar situaciones de repudio. * </p> * <p> * Una pecualiaridad de las firmas trifásicas PAdES es que en la generación o firma de un PDF se genera de forma * automática un identificador único y aleatorio llamado <i>FILE_ID</i>, que hace que al firmar en momentos diferentes * dos PDF exactamente iguales se generen PDF con un <i>FILE_ID</i> distinto, y, por lo tanto, con la huella * digital de la firma electrónica distinta.<br> * Para solventar este inconveniente, en la firma trifásica PDF, se considera prefirma tanto la totalidad de los atributos * CAdES a firmar como el <i>FILE_ID</i> del PDF que se debe compartir entre pre-firma y post-firma. * </p> * Notas sobre documentos <i>certificados</i>:<br> * Si un PDF firmado se ha certificado (por ejemplo, añadiendo una firma electrónica usando Adobe Reader), cualquier * modificación posterior del fichero (como la adición de nuevas firmas con este método) invalidará * las firmas previamente existentes.<br> * Consulte la documentación de la opción <code>allowSigningCertifiedPdfs</code> para establecer un comportamiento por * defecto respecto a los PDF certificados. * @author Tomás García-Merás */ public final class PAdESTriPhaseSigner { private static final String PDF_OID = "1.2.826.0.1089.1.5"; //$NON-NLS-1$ private static final String PDF_DESC = "Documento en formato PDF"; //$NON-NLS-1$ /** Referencia a la última página del documento PDF. */ public static final int LAST_PAGE = -666; /** Versión de iText necesaria para el uso de esta clase (2.1.7). */ public static final String ITEXT_VERSION = "2.1.7"; //$NON-NLS-1$ private static final int CSIZE = 27000; private PAdESTriPhaseSigner() { // No permitimos la instanciacion } /** Obtiene la pre-firma PAdES/CAdES de un PDF (atributos CAdES a firmar) * @param digestAlgorithmName Nombre del algoritmo de huella digital usado para la firma. Debe usarse exactamente el mismo valor en la post-firma. * <p>Se aceptan los siguientes algoritmos en el parámetro <code>digestAlgorithmName</code>:</p> * <ul> * <li><i>SHA1</i></li> * <li><i>MD5</i> (no recomendado por vulnerable)</li> * <li><i>MD2</i> (no recomendado por vulnerable)</li> * <li><i>SHA-256</i></li> * <li><i>SHA-384</i></li> * <li><i>SHA-512</i></li> * </ul> * @param inPDF PDF a firmar. Debe usarse exactamente el mismo documento en la post-firma. * @param signerCertificateChain Cadena de certificados del firmante Debe usarse exactamente la misma cadena de certificados en la post-firma. * @param xParams Parámetros adicionales para la firma (<a href="doc-files/extraparams.html">detalle</a>). Deben usarse exactamente los mismos valores en la post-firma. * @param signTime Momento de la firma. Debe usarse exactamente el mismo valor en la post-firma. * @return pre-firma CAdES/PAdES (atributos CAdES a firmar) * @throws IOException En caso de errores de entrada / salida * @throws AOException En caso de cualquier otro tipo de error * @throws DocumentException En caso de errores en el XML de sesión */ public static PdfSignResult preSign(final String digestAlgorithmName, final byte[] inPDF, final Certificate[] signerCertificateChain, final GregorianCalendar signTime, final Properties xParams) throws IOException, AOException, DocumentException { final Properties extraParams = xParams != null ? xParams : new Properties(); final PdfTriPhaseSession ptps = PdfSessionManager.getSessionData(inPDF, signerCertificateChain, signTime, extraParams); // La norma PAdES establece que si el algoritmo de huella digital es SHA1 debe usarse SigningCertificateV2, y en cualquier // otro caso deberia usarse SigningCertificateV2 boolean signingCertificateV2; if (extraParams.containsKey("signingCertificateV2")) { //$NON-NLS-1$ signingCertificateV2 = Boolean.parseBoolean(extraParams.getProperty("signingCertificateV2")); //$NON-NLS-1$ } else { signingCertificateV2 = !"SHA1".equals(AOSignConstants.getDigestAlgorithmName(digestAlgorithmName)); //$NON-NLS-1$ } final byte[] original = AOUtil.getDataFromInputStream(ptps.getSAP().getRangeStream()); // Calculamos el MessageDigest final byte[] md; try { md = MessageDigest.getInstance(AOSignConstants.getDigestAlgorithmName(digestAlgorithmName)) .digest(original); } catch (final NoSuchAlgorithmException e) { throw new AOException("El algoritmo de huella digital no es valido: " + e, e); //$NON-NLS-1$ } // Pre-firma CAdES return new PdfSignResult(ptps.getFileID(), CAdESTriPhaseSigner.preSign(AOSignConstants.getDigestAlgorithmName(digestAlgorithmName), // Algoritmo de huella digital null, // Datos a firmar (null por ser explicita)) signerCertificateChain, // Cadena de certificados del firmante AdESPolicy.buildAdESPolicy(extraParams), // Politica de firma signingCertificateV2, // signingCertificateV2 md, // Valor de la huella digital del contenido signTime.getTime(), // Fecha de la firma (debe establecerse externamente para evitar desincronismos en la firma trifasica) true, // Modo PAdES PDF_OID, PDF_DESC, CommitmentTypeIndicationsHelper.getCommitmentTypeIndications(extraParams), CAdESSignerMetadataHelper.getCAdESSignerMetadata(extraParams)), null, // Sello de tiempo signTime, extraParams); } /** Post-firma en PAdES un documento PDF a partir de una pre-firma y la firma PKCS#1, generando un PDF final completo. * @param digestAlgorithmName Nombre del algoritmo de huella digital usado para la firma (debe ser el mismo que el usado en la pre-firma). * <p>Se aceptan los siguientes algoritmos en el parámetro <code>digestAlgorithmName</code>:</p> * <ul> * <li><i>SHA1</i></li> * <li><i>MD5</i> (no recomendado por vulnerable)</li> * <li><i>MD2</i> (no recomendado por vulnerable)</li> * <li><i>SHA-256</i></li> * <li><i>SHA-384</i></li> * <li><i>SHA-512</i></li> * </ul> * @param inPdf PDF a firmar (debe ser el mismo que el usado en la pre-firma). * @param signerCertificateChain Cadena de certificados del firmante (debe ser la misma que la usado en la pre-firma). * @param pkcs1Signature Resultado de la firma PKCS#1 v1.5 de los datos de la pre-firma. * @param preSign Resultado de la pre-firma * @param enhancer Manejador para la generación de nuevos modos de firma (con * sello de tiempo, archivo longevo, etc.) * @param enhancerConfig Configuración para generar el nuevo modo de firma. * @return PDF firmado * @throws AOException en caso de cualquier tipo de error * @throws IOException Cuando ocurre algun error en la conversión o generación * de estructuras. * @throws NoSuchAlgorithmException Si hay problemas con el algoritmo durante el sello de tiempo. */ public static byte[] postSign(final String digestAlgorithmName, final byte[] inPdf, final X509Certificate[] signerCertificateChain, final byte[] pkcs1Signature, final PdfSignResult preSign, final SignEnhancer enhancer, final Properties enhancerConfig) throws AOException, IOException, NoSuchAlgorithmException { // Obtenemos la firma final PdfSignResult completePdfSSignature = generatePdfSignature(digestAlgorithmName, signerCertificateChain, preSign.getExtraParams(), pkcs1Signature, preSign.getSign(), preSign.getFileID(), preSign.getTimestamp(), preSign.getSignTime(), enhancer, enhancerConfig); // Insertamos la firma en el PDF return insertSignatureOnPdf(inPdf, signerCertificateChain, completePdfSSignature); } private static PdfSignResult generatePdfSignature(final String digestAlgorithmName, final X509Certificate[] signerCertificateChain, final Properties xParams, final byte[] pkcs1Signature, final byte[] signedAttributes, final String pdfFileId, final byte[] timestamp, final GregorianCalendar signingTime, final SignEnhancer enhancer, final Properties enhancerConfig) throws AOException, IOException, NoSuchAlgorithmException { byte[] completeCAdESSignature = CAdESTriPhaseSigner.postSign( AOSignConstants.getDigestAlgorithmName(digestAlgorithmName), null, signerCertificateChain, pkcs1Signature, signedAttributes); final Properties extraParams = xParams != null ? xParams : new Properties(); //***************** SELLO DE TIEMPO **************** TsaParams tsaParams; try { tsaParams = new TsaParams(extraParams); } catch (final Exception e) { tsaParams = null; } if (tsaParams != null) { completeCAdESSignature = new CMSTimestamper(tsaParams).addTimestamp(completeCAdESSignature, tsaParams.getTsaHashAlgorithm(), signingTime); } //************** FIN SELLO DE TIEMPO **************** if (enhancer != null) { completeCAdESSignature = enhancer.enhance(completeCAdESSignature, enhancerConfig); } return new PdfSignResult(pdfFileId, completeCAdESSignature, timestamp, // Sello de tiempo signingTime, xParams != null ? xParams : new Properties()); } private static byte[] insertSignatureOnPdf(final byte[] inPdf, final X509Certificate[] signerCertificateChain, final PdfSignResult signature) throws AOException, IOException { final byte[] outc = new byte[CSIZE]; if (signature.getSign().length > CSIZE) { throw new AOException("El tamano de la firma (" + signature.getSign().length //$NON-NLS-1$ + ") supera el maximo permitido para un PDF (" + CSIZE + ")" //$NON-NLS-1$ //$NON-NLS-2$ ); } final PdfDictionary dic2 = new PdfDictionary(); System.arraycopy(signature.getSign(), 0, outc, 0, signature.getSign().length); dic2.put(PdfName.CONTENTS, new PdfString(outc).setHexWriting(true)); final PdfTriPhaseSession pts; try { pts = PdfSessionManager.getSessionData(inPdf, signerCertificateChain, signature.getSignTime(), signature.getExtraParams()); } catch (final DocumentException e1) { throw new IOException(e1); } final PdfSignatureAppearance sap = pts.getSAP(); final ByteArrayOutputStream baos = pts.getBAOS(); final String badFileID = pts.getFileID(); try { sap.close(dic2); } catch (final Exception e) { baos.close(); throw new AOException("Error al cerrar el PDF para finalizar el proceso de firma", e); //$NON-NLS-1$ } final byte[] ret = new String(baos.toByteArray(), "ISO-8859-1").replace(badFileID, signature.getFileID()) //$NON-NLS-1$ .getBytes("ISO-8859-1"); //$NON-NLS-1$ baos.close(); return ret; } }