All Downloads are FREE. Search and download functionalities are using the official Maven repository.

es.gob.afirma.signers.pades.AOPDFSigner Maven / Gradle / Ivy

/* 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.
 * You may contact the copyright holder at: [email protected]
 */

package es.gob.afirma.signers.pades;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.logging.Logger;

import com.aowagie.text.exceptions.BadPasswordException;
import com.aowagie.text.pdf.AcroFields;
import com.aowagie.text.pdf.PdfDictionary;
import com.aowagie.text.pdf.PdfName;
import com.aowagie.text.pdf.PdfPKCS7;
import com.aowagie.text.pdf.PdfReader;

import es.gob.afirma.core.AOCancelledOperationException;
import es.gob.afirma.core.AOException;
import es.gob.afirma.core.AOInvalidFormatException;
import es.gob.afirma.core.misc.AOUtil;
import es.gob.afirma.core.signers.AOPkcs1Signer;
import es.gob.afirma.core.signers.AOSignConstants;
import es.gob.afirma.core.signers.AOSignInfo;
import es.gob.afirma.core.signers.AOSigner;
import es.gob.afirma.core.signers.AOSimpleSignInfo;
import es.gob.afirma.core.signers.CounterSignTarget;
import es.gob.afirma.core.signers.SignEnhancer;
import es.gob.afirma.core.ui.AOUIFactory;
import es.gob.afirma.core.util.tree.AOTreeModel;
import es.gob.afirma.core.util.tree.AOTreeNode;

/** Manejador de firmas binarias de ficheros Adobe PDF en formato PAdES.
 * 

Para compatibilidad estricta con PAdES-BES/EPES se utiliza ETSI.CAdES.detached como nombre del subfiltro.

*

La compatibilidad con PAdES no es completa, omitiéndose los siguientes aspectos de la normativa:

*
    *
  • Firma separada de ficheros empotrados en el documento PDF.
  • *
  • Firma separada de ficheros adjuntos al documento PDF.
  • *
*

* Estas mismas deficiencias provocan igualmente la incompatibilidad de las firmas generadas con "Carpetas PDF" (Portfolios PDF). * Cuando se encuentran documentos PDF con ficheros adjuntos o empotrados se imprime información relativa en consola. *

*

* Por compatibilidad con Adobe Reader, la firmas se generan con el subfiltro "adbe.pkcs7.detached" en vez de con * "ETSI.CAdES.detached". Consulte la documentación del parámetro signatureSubFilter para variar este comportamiento. *

*

* La clase necesita específicamente la versión de iText 2.1.7 modificada para el Cliente @firma. *

*/ public final class AOPDFSigner implements AOSigner { private static final String PDF_FILE_SUFFIX = ".pdf"; //$NON-NLS-1$ private static final String PDF_FILE_HEADER = "%PDF-"; //$NON-NLS-1$ private static final Logger LOGGER = Logger.getLogger("es.gob.afirma"); //$NON-NLS-1$ private static final PdfName PDFNAME_ETSI_RFC3161 = new PdfName("ETSI.RFC3161"); //$NON-NLS-1$ private static final PdfName PDFNAME_DOCTIMESTAMP = new PdfName("DocTimeStamp"); //$NON-NLS-1$ /** Tamaño mínimo de un PDF. * * https://stackoverflow.com/questions/17279712/what-is-the-smallest-possible-valid-pdf * . */ private static final int PDF_MIN_FILE_SIZE = 70; private static SignEnhancer enhancer = null; private static Properties enhancerConfig; static { enhancerConfig = new Properties(); String enhancerClassName = null; try { enhancerConfig.load( AOPDFSigner.class.getResourceAsStream("/enhancer.properties") //$NON-NLS-1$ ); enhancerClassName = enhancerConfig.getProperty("enhancerClassFile"); //$NON-NLS-1$ if (enhancerClassName != null) { enhancer = (SignEnhancer) Class.forName(enhancerClassName).getConstructor().newInstance(); LOGGER.info("Se usara el siguiente mejorador de firmas: " + enhancerClassName); //$NON-NLS-1$ } } catch(final ClassNotFoundException e) { LOGGER.warning( "Se ha configurado la clase de mejora '" + enhancerClassName + "', pero esta no se encuentra: " + e //$NON-NLS-1$//$NON-NLS-2$ ); } catch (final Exception e) { LOGGER.info("No hay un mejorador de firmas correctamente instalado: " + e); //$NON-NLS-1$ } } /** Obtiene el mejorador de firmas por defecto. * @return Mejorador de firmas por defecto. */ public static SignEnhancer getSignEnhancer() { return enhancer; } /** Obtiene la configuración del mejorador de firmas por defecto. * @return Configuración del mejorador de firmas por defecto. */ public static Properties getSignEnhancerConfig() { return enhancerConfig; } /** Firma un documento PDF en formato PAdES. *

* Notas sobre documentos certificados:
* Si un PDF firmado se ha certificado (por ejemplo, añadiendo una firma electrónica usando Adobe Acrobat), cualquier * modificación posterior del fichero (como la adición de nuevas firmas con este método) invalidará * las firmas previamente existentes.
* Si se detecta un documento PDF certificado, se mostrará un diálogo gráfico advirtiendo al usuario de esta * situación y pidiendo confirmación para continuar.
Si desea evitar interacciones directas con los usuarios * consulte la documentación de las opciones allowSigningCertifiedPdfs y headless.
*

*

* Notas sobre documentos protegidos con contraseña:
* Si un PDF está protegido con contraseña por estar cifrado, se mostrará un diálogo gráfico advirtiendo al usuario de esta * situación y solicitando la contraseña de apertura del PDF.
Si desea evitar interacciones directas con los usuarios * consulte la documentación de las opciones ownerPassword y headless. * Adicionalmente, si el fichero de entrada estaba cifrado y protegido con contraseña, la salida seráa un documento PDF * igualmente cifrado y protegido con contraseña.. *

* @param inPDF Documento PDF a firmar. * @param algorithm Algoritmo a usar para la firma. *

Se aceptan los siguientes algoritmos en el parámetro algorithm:

*
    *
  • SHA1withRSA
  • *
  • SHA256withRSA
  • *
  • SHA384withRSA
  • *
  • SHA512withRSA
  • *
* @param key Clave privada a usar para firmar. * @param certChain Cadena de certificados del firmante. * @param xParams Parámetros adicionales para la firma (detalle). * @return Documento PDF firmado en formato PAdES. * @throws AOException Cuando ocurre cualquier problema durante el proceso. * @throws IOException Cuando hay errores en el tratamiento de datos. */ @Override public byte[] sign(final byte[] inPDF, final String algorithm, final PrivateKey key, final java.security.cert.Certificate[] certChain, final Properties xParams) throws AOException, IOException { final Properties extraParams = xParams != null ? xParams : new Properties(); final java.security.cert.Certificate[] certificateChain = Boolean.parseBoolean(extraParams.getProperty(PdfExtraParams.INCLUDE_ONLY_SIGNNING_CERTIFICATE, Boolean.FALSE.toString())) ? new X509Certificate[] { (X509Certificate) certChain[0] } : certChain; final GregorianCalendar signTime = PdfUtil.getSignTime(extraParams.getProperty(PdfExtraParams.SIGN_TIME)); // Sello de tiempo byte[] data; try { data = PdfTimestamper.timestampPdf(inPDF, extraParams, signTime); } catch (final NoSuchAlgorithmException e1) { throw new IOException( "No se soporta el algoritmo indicado para la huella digital del sello de tiempo: " + e1, e1 //$NON-NLS-1$ ); } // Prefirma final PdfSignResult pre; try { pre = PAdESTriPhaseSigner.preSign( algorithm, data, certificateChain, signTime, extraParams ); } catch (final InvalidPdfException e) { throw e; } // Firma PKCS#1 final byte[] interSign; try { interSign = new AOPkcs1Signer().sign( pre.getSign(), algorithm, key, certificateChain, extraParams ); } catch (final Exception e) { if ("es.gob.jmulticard.CancelledOperationException".equals(e.getClass().getName())) { //$NON-NLS-1$ throw new AOCancelledOperationException(); } throw new AOException("Error durante la firma PAdES: " + e, e); //$NON-NLS-1$ } // Postfirma try { return PAdESTriPhaseSigner.postSign( algorithm, data, certificateChain, interSign, pre, getSignEnhancer(), // SignEnhancer getSignEnhancerConfig() // EnhancerConfig (si le llega null usa los ExtraParams) ); } catch (final NoSuchAlgorithmException e) { throw new AOException("Error el en algoritmo de firma: " + e, e); //$NON-NLS-1$ } } /** Añade una firma PAdES a un documento PDF. El comportamiento es exactamente el mismo que una llamada al método sign(...) * puesto que las multifirmas en los ficheros PDF se limitan a firmas independientes "en serie", pero no implementando los mecanismos de * cofirma o contrafirma de CAdES. *

* Notas sobre documentos certificados:
* 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.
* Si se detecta un documento PDF certificado, se mostrará un diálogo gráfico advirtiendo al usuario de esta * situación y pidiendo confirmación para continuar.
Si desea evitar interacciones directas con los usuarios * consulte la documentación de las opciones allowSigningCertifiedPdfs y headless.
*

*

* Notas sobre documentos protegidos con contraseña:
* Si un PDF está protegido con contraseña por estar cifrado, se mostrará un diálogo gráfico advirtiendo al usuario de esta * situación y solicitando la contraseña de apertura del PDF.
Si desea evitar interacciones directas con los usuarios * consulte la documentación de las opciones ownerPassword y headless. * Adicionalmente, si el fichero de entrada estaba cifrado y protegido con contraseña, la salida será un documento PDF * igualmente cifrado y protegido con contraseña. *

* En general, es recomendable prescindir de este método y llamar directamente al método sign(...). * @param data Se ignora el valor de este parámetro. El documento PDF debe proporcionarse mediante el parátro sign. * @param sign Documento PDF a firmar. * @param algorithm Algoritmo a usar para la firma. *

Se aceptan los siguientes algoritmos en el parámetro algorithm:

*
    *
  • SHA1withRSA
  • *
  • SHA256withRSA
  • *
  • SHA384withRSA
  • *
  • SHA512withRSA
  • *
* @param key Clave privada a usar para firmar. * @param certChain Cadena de certificados del firmante. * @param extraParams Parámetros adicionales para la firma (detalle). * @return Documento PDF firmado en formato PAdES. * @throws AOException Cuando ocurre cualquier problema durante el proceso. * @throws IOException En caso de errores de entrada / salida. */ @Override public byte[] cosign(final byte[] data, final byte[] sign, final String algorithm, final PrivateKey key, final java.security.cert.Certificate[] certChain, final Properties extraParams) throws AOException, IOException { return sign(sign, algorithm, key, certChain, extraParams); } /** Añade una firma PAdES a un documento PDF. El comportamiento es exactamente el mismo que una llamada al método sign(...) * puesto que las multifirmas en los ficheros PDF se limitan a firmas independientes "en serie", pero no implementando los mecanismos de * cofirma o contrafirma de CAdES. *

* Notas sobre documentos certificados:
* 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.
* Si se detecta un documento PDF certificado, se mostrará un diálogo gráfico advirtiendo al usuario de esta * situación y pidiendo confirmación para continuar.
Si desea evitar interacciones directas con los usuarios * consulte la documentación de las opciones allowSigningCertifiedPdfs y headless.
*

*

* Notas sobre documentos protegidos con contraseña:
* Si un PDF está protegido con contraseña por estar cifrado, se mostrará un diálogo gráfico advirtiendo al usuario de esta * situación y solicitando la contraseña de apertura del PDF.
Si desea evitar interacciones directas con los usuarios * consulte la documentación de las opciones ownerPassword y headless. * Adicionalmente, si el fichero de entrada estaba cifrado y protegido con contraseña, la salida será un documento PDF * igualmente cifrado y protegido con contraseña. *

* En general, es recomendable prescindir de este método y llamar directamente al método sign(...) * @param sign Documento PDF a firmar * @param algorithm Algoritmo a usar para la firma. *

Se aceptan los siguientes algoritmos en el parámetro algorithm:

*
    *
  • SHA1withRSA
  • *
  • SHA256withRSA
  • *
  • SHA384withRSA
  • *
  • SHA512withRSA
  • *
* @param key Clave privada a usar para firmar. * @param certChain Cadena de certificados del firmante. * @param extraParams Parámetros adicionales para la firma (detalle). * @return Documento PDF firmado en formato PAdES. * @throws AOException Cuando ocurre cualquier problema durante el proceso. * @throws IOException En caso de errores de entrada / salida. */ @Override public byte[] cosign(final byte[] sign, final String algorithm, final PrivateKey key, final java.security.cert.Certificate[] certChain, final Properties extraParams) throws AOException, IOException { return sign(sign, algorithm, key, certChain, extraParams); } /** Operación no soportada para firmas PAdES. */ @Override public byte[] countersign(final byte[] sign, final String algorithm, final CounterSignTarget targetType, final Object[] targets, final PrivateKey key, final java.security.cert.Certificate[] certChain, final Properties extraParams) { throw new UnsupportedOperationException("No es posible realizar contrafirmas de ficheros PDF"); //$NON-NLS-1$ } /** Devuelve el nombre de fichero de firma predeterminado que se recomienda usar para * un PDF firmado con nombre original igual al proporcionado. * En este caso el resultado será siempre el nombre original más un * sufijo adicional (opcional) previo a la extensión. * Siempre se termina el nombre de fichero con la extensión .pdf, incluso si el nombre original carecía de esta. * @param originalName Nombre del fichero original que se firma. * @param inText Sufijo a agregar al nombre de fichero devuelto, inmediatamente anterior a la extensión. * @return Nombre apropiado para el fichero de firma. */ @Override public String getSignedName(final String originalName, final String inText) { final String inTextInt = inText != null ? inText : ""; //$NON-NLS-1$ if (originalName == null) { return "signed.pdf"; //$NON-NLS-1$ } if (originalName.toLowerCase(Locale.US).endsWith(PDF_FILE_SUFFIX)) { return originalName.substring(0, originalName.length() - PDF_FILE_SUFFIX.length()) + inTextInt + PDF_FILE_SUFFIX; } return originalName + inTextInt + PDF_FILE_SUFFIX; } /** Recupera el árbol de nodos de firma de una firma electrónica. * Los nodos del árbol serán textos con el CommonName (CN X.500) * del titular del certificado u objetos de tipo AOSimpleSignInfo con la * información básica de las firmas individuales, según * el valor del parámetro asSimpleSignInfo. Los nodos se * mostrarán en el mismo orden y con la misma estructura con el que * aparecen en la firma electrónica.
* La propia estructura de firma se considera el nodo raíz, la firma y cofirmas * penderán directamentede de este. * @param sign Firma electrónica de la que se desea obtener la estructura. * @param asSimpleSignInfo Si es true se devuelve un árbol con la * información básica de cada firma individual * mediante objetos AOSimpleSignInfo, si es false * un árbol con los nombres (CN X.500) de los titulares certificados. * @return Árbol de nodos de firma o null en caso de error. */ @Override public AOTreeModel getSignersStructure(final byte[] sign, final boolean asSimpleSignInfo) { final AOTreeNode root = new AOTreeNode("Datos"); //$NON-NLS-1$ if (!isPdfFile(sign)) { return new AOTreeModel(root); } PdfReader pdfReader; try { pdfReader = new PdfReader(sign); } catch (final BadPasswordException e) { LOGGER.info( "El PDF necesita contrasena: " + e //$NON-NLS-1$ ); try { pdfReader = new PdfReader( sign, new String( AOUIFactory.getPassword( CommonPdfMessages.getString("AOPDFSigner.0"), //$NON-NLS-1$ null ) ).getBytes() ); } catch (final BadPasswordException e2) { LOGGER.severe("La contrasena del PDF no es valida, se devolvera un arbol vacio: " + e2); //$NON-NLS-1$ return new AOTreeModel(root); } catch (final Exception e3) { LOGGER.severe("No se ha podido leer el PDF, se devolvera un arbol vacio: " + e3); //$NON-NLS-1$ return new AOTreeModel(root); } } catch (final Exception e) { LOGGER.severe("No se ha podido leer el PDF, se devolvera un arbol vacio: " + e); //$NON-NLS-1$ return new AOTreeModel(root); } final AcroFields af; try { af = pdfReader.getAcroFields(); } catch (final Exception e) { LOGGER.severe("No se ha podido obtener la informacion de los firmantes del PDF, se devolvera un arbol vacio: " + e); //$NON-NLS-1$ return new AOTreeModel(root); } final List names = af.getSignatureNames(); for (final String signatureName : names) { // Comprobamos si es una firma o un sello final PdfDictionary pdfDictionary = af.getSignatureDictionary(signatureName); if (PDFNAME_ETSI_RFC3161.equals(pdfDictionary.get(PdfName.SUBFILTER)) || PDFNAME_DOCTIMESTAMP.equals(pdfDictionary.get(PdfName.SUBFILTER))) { // Ignoramos los sellos continue; } final PdfPKCS7 pcks7; try { pcks7 = af.verifySignature(signatureName); } catch(final Exception e) { LOGGER.severe( "El PDF contiene una firma corrupta o con un formato desconocido (" + //$NON-NLS-1$ signatureName + "), se continua con las siguientes si las hubiese: " + e //$NON-NLS-1$ ); continue; } if (asSimpleSignInfo) { final X509Certificate[] certChain = new X509Certificate[pcks7.getSignCertificateChain().length]; for (int j = 0; j < certChain.length; j++) { certChain[j] = (X509Certificate) pcks7.getSignCertificateChain()[j]; } final AOSimpleSignInfo ssi = new AOSimpleSignInfo( certChain, pcks7.getSignDate().getTime() ); // Extraemos el PKCS#1 de la firma final byte[] pkcs1 = pcks7.getPkcs1(); if (pkcs1 != null) { ssi.setPkcs1(pkcs1); } root.add(new AOTreeNode(ssi)); } else { root.add(new AOTreeNode(AOUtil.getCN(pcks7.getSigningCertificate()))); } } return new AOTreeModel(root); } /** Comprueba que los datos proporcionados sean un documento PDF. * @param data Datos a comprobar. * @return true si los datos proporcionados son un documento PDF, * false en caso contrario. */ @Override public boolean isSign(final byte[] data) { if (data == null) { LOGGER.warning("Se han introducido datos nulos para su comprobacion"); //$NON-NLS-1$ return false; } if (!isPdfFile(data)) { return false; } final Object root = getSignersStructure(data, false).getRoot(); if (root instanceof AOTreeNode) { // Si el arbol contiene firmas... if (AOTreeModel.getChildCount(root) > 0) { return true; } // Si no las contiene aun puede haber firmas no registradas // Como el metodo no recibe "extraParams" buscamos en las propiedades de sistema final Properties extraParams = System.getProperties(); try { if (PdfUtil.pdfHasUnregisteredSignatures(data, extraParams) && Boolean.TRUE.toString().equalsIgnoreCase(extraParams.getProperty(PdfExtraParams.ALLOW_COSIGNING_UNREGISTERED_SIGNATURES))) { return true; } } catch (final Exception e) { LOGGER.severe("No se han podido comprobar las firmas no registradas del PDF: " + e); //$NON-NLS-1$ } } return false; } private static boolean isPdfFile(final byte[] data) { if (data == null || data.length < PDF_MIN_FILE_SIZE) { return false; } final byte[] buffer = new byte[PDF_FILE_HEADER.length()]; try { new ByteArrayInputStream(data).read(buffer); } catch (final Exception e) { Logger.getLogger("es.gob.afirma").warning( //$NON-NLS-1$ "El contenido parece corrupto o truncado: " + e //$NON-NLS-1$ ); return false; } // Comprobamos que cuente con una cabecera PDF if (!PDF_FILE_HEADER.equals(new String(buffer))) { return false; } try { // Si lanza una excepcion al crear la instancia, no es un fichero PDF new PdfReader(data); } catch (final BadPasswordException e) { LOGGER.warning("El PDF esta protegido con contrasena, se toma como PDF valido: " + e); //$NON-NLS-1$ return true; } catch (final Exception e) { return false; } return true; } /** Comprueba que los datos proporcionados sean un documento PDF. * @param data Datos a comprobar * @return true si los datos proporcionados son un documento PDF, false en caso contrario */ @Override public boolean isValidDataFile(final byte[] data) { if (data == null) { LOGGER.warning("Se han introducido datos nulos para su comprobacion"); //$NON-NLS-1$ return false; } return isPdfFile(data); } /** Obtiene el nombre con el que debería guardarse un PDF tras ser * firmado. Básicamente se le anexa el sufijo .signed al * nombre original, manteniendo la extensión (se respetan * mayúculas y minúsculas en esta, pero no se admite una * extensión con mezcla de ambas). * @param originalName Nombre original del fichero PDF. * @return Nombre recomendado para el PDF ya firmado. */ public static String getSignedName(final String originalName) { if (originalName == null) { return "signed.pdf"; //$NON-NLS-1$ } if (originalName.endsWith(PDF_FILE_SUFFIX)) { return originalName.replace(PDF_FILE_SUFFIX, ".signed.pdf"); //$NON-NLS-1$ } if (originalName.endsWith(".PDF")) { //$NON-NLS-1$ return originalName.replace(".PDF", ".signed.pdf"); //$NON-NLS-1$ //$NON-NLS-2$ } return originalName + ".signed.pdf"; //$NON-NLS-1$ } /** Si la entrada es un documento PDF, devuelve el mismo documento PDF. * @param sign Documento PDF * @return Mismo documento PDF de entrada, sin modificar en ningú aspecto. * @throws AOInvalidFormatException Si los datos de entrada no son un documento PDF. */ @Override public byte[] getData(final byte[] sign) throws AOInvalidFormatException { // Si no es una firma PDF valida, lanzamos una excepcion if (!isSign(sign)) { throw new AOInvalidFormatException("El documento introducido no contiene una firma valida"); //$NON-NLS-1$ } // TODO: Devolver el PDF sin firmar return sign; } /** Si la entrada es un documento PDF, devuelve un objeto AOSignInfo * con el formato establecido a AOSignConstants.SIGN_FORMAT_PDF. * @param data Documento PDF. * @return Objeto AOSignInfo con el formato establecido a AOSignConstants.SIGN_FORMAT_PDF. * @throws AOException Si los datos de entrada no son un documento PDF. */ @Override public AOSignInfo getSignInfo(final byte[] data) throws AOException { if (data == null) { throw new IllegalArgumentException("No se han introducido datos para analizar"); //$NON-NLS-1$ } if (!isSign(data)) { throw new AOInvalidFormatException("Los datos introducidos no se corresponden con un objeto de firma"); //$NON-NLS-1$ } return new AOSignInfo(AOSignConstants.SIGN_FORMAT_PDF); // Aqui podria venir el analisis de la firma buscando alguno de los // otros datos de relevancia que se almacenan en el objeto AOSignInfo } /** Configura, cuando no lo esten ya, las propiedades necesarias para que las firmas * sobre unos datos respeten el formato que tuviesen firmas anteriores. * @param data Datos que se desean firmar. * @param config Configuración establecida. */ public static void configureRespectfulProperties(final byte[] data, final Properties config) { if (config != null && !config.containsKey(PdfExtraParams.SIGNATURE_SUBFILTER)) { String filter; try { filter = PdfUtil.getFirstSupportedSignSubFilter(data, config); } catch (final Exception e) { LOGGER.warning("Error al configurar la firma PDF para que sea igual a las existentes: " + e); //$NON-NLS-1$ return; } if (filter != null) { config.setProperty(PdfExtraParams.SIGNATURE_SUBFILTER, filter.substring(filter.indexOf('/') + 1)); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy