Firmar y verificar firma digital en PDFs mediante Java con iText

Ya hemos hablado anteriormente sobre cómo empezar a utilizar certificados digitales en un entorno de desarrollo de programación, ahora vamos a ver la primera aplicación real con éstos certificados.

Firma digital de documentos PDF

La firma verifica la autoría y validez de una persona, por lo que es especialmente útil cuando hablamos de PDF que contienen facturas o documentos oficiales.

También lo podemos usar para validar un certificado en entornos Web de manera transparente al navegador. Quien lo haya hecho, sabrá que si queremos usar certificados en una aplicación web, hacerlo funcionar en todos los navegadores web puede causar bastantes dolores de cabeza, pero una sencilla manera es firmar un PDF dummy mediante CryptoApplet y validar si esa firma es válida, pero este procedimiento ya lo trataremos más adelante en otras entradas de blog.

Creación y modificación de PDFs con la librería iText

La librería iText disponible para Java y .NET permite crear y modificar PDFs dinámicamente desde un entorno de programación. En nuestro caso vamos a descargarnos las librerías OpenSource y añadirlas a nuestro proyecto Java, en nuestro caso hemos utlizado NetBeans.

Creando PDFs dinámicamente mediante PHP

En anteriores entradas del blog ya tratamos la manera de cómo crear PDFs dinámicamente mediante la librería mPDF, que podría ser interesante para esta aplicación.

Preparando nuestro certificado digital

El paso previo es tener preparado nuestro certificado digital. Éste debe estar en formato con extensión PFX (estándar PKCS12), que en el caso de no tener ninguno disponible, ya hemos explicado anteriormente en el artículo cómo crear certificados digitales como crear uno.

Un paso previo muy importante, a la finalización de nuestro proyecto, es añadir la autoridad certificadora a nuestra KeyStore de Java. En el caso de seguir el artículo de blog será obligado hacerlo si queremos que funcione correctamente, al igual que si por ejemplo queremos utilizar un certificado digital del DNIe, ya que el KeyStore de Java sólo contempla los típicos usados para HTTPS y algunos otros como Verisign.

Podéis encontrar más información de cómo añadir autoridades certificadoras a nuestro KeyStore de Java en este artículo de blog.

Firmando PDFs en Java

Crearemos el proyecto en NetBeans PDFSigner y pegaremos en el Main.java el siguiente código:

package pdfsigner;

import java.security.*;
import java.security.cert.Certificate;
import java.io.*;
import com.lowagie.text.pdf.*;
import com.lowagie.text.*;

/**
 * @author Imaginanet
 */
public class Main {

    public static void main(String[] args) {
        try {

            if(args.length == 0) {
                System.out.print("Necesito el nombre del PDF a firmar");
                System.exit(1);
            }

            KeyStore ks = KeyStore.getInstance("pkcs12");
            ks.load(new FileInputStream("RUTA_CERTIFICADO_PFX"), "CLAVE_PRIVADA_CERTIFICADO".toCharArray());
            String alias = (String)ks.aliases().nextElement();
            PrivateKey key = (PrivateKey)ks.getKey(alias, "CLAVE_PRIVADA_CERTIFICADO".toCharArray());
            Certificate[] chain = ks.getCertificateChain(alias);
            // Recibimos como parámetro de entrada el nombre del archivo PDF a firmar
            PdfReader reader = new PdfReader(args[0]); 
            FileOutputStream fout = new FileOutputStream("RUTA_ARCHIVO_PDF_FIRMADO");

            // Añadimos firma al documento PDF
            PdfStamper stp = PdfStamper.createSignature(reader, fout, '?');
            PdfSignatureAppearance sap = stp.getSignatureAppearance();
            sap.setCrypto(key, chain, null, PdfSignatureAppearance.WINCER_SIGNED);
            sap.setReason("Firma PKCS12");
            sap.setLocation("Imaginanet");
            // Añade la firma visible. Podemos comentarla para que no sea visible.
            sap.setVisibleSignature(new Rectangle(100,100,200,200),1,null);
            stp.close();
        }
        catch(Exception e) {
            e.printStackTrace();
        }

    }

}

Donde esperamos como parámetro de entrada al programa la ruta al archivo PDF original sin firmar y modificar las rutas al certificado digital PFX y la clave privada de éste.

Verificando firmas en PDFs desde Java

Ahora comprobaremos la vericidad de la firma con otro proyecto NetBeans PDFVerifier y colocaremos en su Main.java:

package pdfverifier;

import java.security.*;
import java.security.cert.Certificate;
import java.io.*;
import com.lowagie.text.pdf.*;
import java.util.Calendar;
import java.util.ArrayList;
import java.util.Random;

/**
 * @author Imaginanet
 */
public class Main {

    public static void main(String[] args) {
        try {
            if(args.length == 0) {
                System.out.print("Necesito el nombre del PDF a comprobar");
                System.exit(1);
            }
 
            Random rnd = new Random();
            KeyStore kall = PdfPKCS7.loadCacertsKeyStore();
            PdfReader reader = new PdfReader(args[0]);
            AcroFields af = reader.getAcroFields();
            ArrayList names = af.getSignatureNames();
            for (int k = 0; k < names.size(); ++k) {
               String name = (String)names.get(k);
               int random = rnd.nextInt();
               FileOutputStream out = new FileOutputStream("revision_" + random + "_" + af.getRevision(name) + ".pdf");

               byte bb[] = new byte[8192];
               InputStream ip = af.extractRevision(name);
               int n = 0;
               while ((n = ip.read(bb)) > 0)
               out.write(bb, 0, n);
               out.close();
               ip.close();

               PdfPKCS7 pk = af.verifySignature(name);
               Calendar cal = pk.getSignDate();
               Certificate pkc[] = pk.getCertificates();
               Object fails[] = PdfPKCS7.verifyCertificates(pkc, kall, null, cal);
               if (fails == null) {
                   System.out.print(pk.getSignName());
               }
               else {
                   System.out.print("Firma no válida");
               }
               File f = new File("revision_" + random + "_" + af.getRevision(name) + ".pdf");
               f.delete();
            }
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }
}

Al igual que el anterior programa, necesita como parámetro de entrada el archivo PDF a comprobar, mostrando en caso de ser verificada la firma los datos del propietario y en caso contrario el mensaje de error "Firma no válida".

Llamada a los programas desde una aplicación

Hemos separado ambos programas para que actúen como programas como cajas negras y sea independiente del lenguaje de programación del que se llame.

En el caso de firmar, llamaremos como java pdfsigner archivo.pdf generando un archivo pdf firmado.

Tras ello, podemos vertificar la firma con java pdfverifier firmado.pdf mostrando la salida de la firma en caso positivo. Ya que la firma puede guardarse en varios formatos según el certificado PFX, recomendamos que esa salida hagamos el parseo necesario desde nuestra aplicación buscando datos que estemos seguros que vayan a salir en la firma, como es el caso del DNI, CIF, dirección email, etc.

Comunicando nuestro programa de firma digital de PDF con aplicaciones PHP

Una de las aplicaciones más interesantes es la de usar esta aplicación de firma con PHP. Las librerías que permiten hacer ésto en PHP no son libres, pero sin embargo podemos llamar a nuestras aplicaciones Java mediante passthru, exec, etc. permitiendo tener firma digital open source en PHP.

Referencias

Comentarios

Comentario de Jose - 13 de Abril de 2012 - 17:34
ayuda!! me da el siguiente error y he tratado ya de todo Exception in thread "main" java.lang.NoClassDefFoundError: org/bouncycastle/tsp/TimeStampTokenInfo at pdfsigner.Main.main(Main.java:36) Caused by: java.lang.ClassNotFoundException: org.bouncycastle.tsp.TimeStampTokenInfo at java.net.URLClassLoader$1.run(URLClassLoader.java:202) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at java.lang.ClassLoader.loadClass(ClassLoader.java:306) at java.lang.ClassLoader.loadClass(ClassLoader.java:247) ... 5 more Java Result: 1
Comentario de Imaginanet - Carlos - 16 de Abril de 2012 - 06:01
Hola Jose, parece que no tienes todas las librerías instaladas. Prueba a instalar las librerías BouncyCastle de http://www.bouncycastle.org/latest_releases.html Saludos.
Comentario de jose - 17 de Abril de 2012 - 18:02
Gracias Carlos, ya pude... ahora bien tengo la duda con el programa que verifica la firma, yo cree un .pfx usando la herramienta makecert de windows, me firmo el documento y todo pero al pasarle el verificador me dice que la firma es invalida, necesito registrar el certificado en algun lugar??? gracias!!1
Comentario de Imaginanet - Carlos - 18 de Abril de 2012 - 05:55
Sí, ya que la autoridad certificadora con la que se creó no está en el KeyStore de Java y por tanto no es válida. Estás en la misma situación y te aconsejo que sigas y leas las siguientes páginas http://www.imaginanet.com/blog/crear-certificados-digitales-con-openssl.html y también http://www.imaginanet.com/blog/anadir-la-autoridad-de-validacion-fnmt-para-el-dni-electronico-al-keystore-de-java.html
Comentario de Rafa - 19 de Abril de 2012 - 15:43
Ayuda A la hora de firmar todo esta bien, me firmó el pdf, pero el problema es cuando voy a verificar la firma, me dice que la firma no es válida. Yo tengo un .p12 que me da una entidad certificadora y con ese realicé la firma, pero a la hora de verificar la firma tengo que comprobar contra el contenededor de certificados de esa entidad, o sea, el "archivo.cer" y no se como hacer eso.
Comentario de Repique - 25 de Abril de 2012 - 23:19
Jose, tengo el mismo problema que tu, me da este error: Exception in thread "main" java.lang.NoClassDefFoundError: org/bouncycastle/tsp/TimeStampTokenInfo at com.itextpdf.text.pdf.AcroFields.verifySignature(AcroFields.java:2307) at com.itextpdf.text.pdf.AcroFields.verifySignature(AcroFields.java:2257) at verificar.Verificar.main(Verificar.java:67) Caused by: java.lang.ClassNotFoundException: org.bouncycastle.tsp.TimeStampTokenInfo at java.net.URLClassLoader$1.run(URLClassLoader.java:366) at java.net.URLClassLoader$1.run(URLClassLoader.java:355) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:354) at java.lang.ClassLoader.loadClass(ClassLoader.java:423) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) at java.lang.ClassLoader.loadClass(ClassLoader.java:356) ... 3 more Java Result: 1 Como lo has solucionado?
Comentario de Repique - 26 de Abril de 2012 - 13:17
Ya lo solucioné añadiendo las librerías bcprov-jdk16-146.jar, bctsp-jdk16-146.jar y bcmail-jdk16-146.jar al proyecto
Comentario de Repique - 26 de Abril de 2012 - 18:53
Ahora me pasa lo mismo que a Rafa y Carlos, no me valida las los pdfs firmados con mi DNIe, incluso después de haber seguido las instrucciones de la página http://www.imaginanet.com/blog/anadir-la-autoridad-de-validacion-fnmt-para-el-dni-electronico-al-keystore-de-java.html Alguien puede ayudarme?
Comentario de Christian - 07 de Mayo de 2012 - 14:01
Tu página me ha ayudado mucho. Muchas gracias
Comentario de Juan Manuel Rivera C - 30 de Julio de 2012 - 13:24
Super el tema que manejas, sin embargo me surje una duda, si el certificado y la llave privada estan en un token hardware, como enviamos dichos valores ya que juridicamente no se pueden tener en el servidor y como dichos elementos no se pueden acceder por ruta ( ej: E:/), como se podria hacer sobre todo en el lado del cliente y que pueda servir en todos los S.O... gracias si me puedes ayudar a despejar esa duda..!
Comentario de Imaginanet - Carlos - 21 de Agosto de 2012 - 09:57
Hola Juan Manuel, supongo que por token hardware te refieres por ejemplo a un DNI electrónico. Para ese caso se necesitaría hacer uso de otra aplicación tipo CryptoApplet, que desde el navegador firma en el lado del cliente con la clave privada y se compara con su clave pública y la Autoridad Certificadora si es correcta en el servidor. Para programarlo es bastante fácil, ya que se puede firmar haciendo uso del software CryptoApplet un PDF genérico y comprobarlo en el lado del servidor, todo automatizado y transparente de cara al usuario, al igual que de manera legal.
Comentario de ALEJANDRO - 20 de Noviembre de 2012 - 15:54
Ayuda no puedo firmar electronicamente ningun documento me aparece este error (***java.lang.Exception)
Comentario de DIEGO - 11 de Marzo de 2013 - 21:26
Ayudaa! copie asi mismo el codigo en java y me falta el paquete pdfsigner... de donde lo descargo ese???/
Comentario de Fatima LOmaquiz - 02 de Junio de 2013 - 19:32
Ayudaa.!! segui todos los pasos de la pagina, agregue mi CA al keytool de java y a la hora de verificar no reconoce mi firma... AYUDA por favor..!!
Comentario de marco - 22 de Octubre de 2013 - 03:27
saludos, una consulta, estoy intentando hacer una aplicacion para que firme y valide documentos pero con los certificados que emite la reniec - peru, mi consulta es que debo de cambiar para que pueda funcionar, supongo que esta entidad debe de tener servicios para la validaciion del certificado, esa es mi consulta, cuando firmo o validos supongo que hago una consulta a un servidor, verdad, como hago eso no pedo entender esa parte.
Comentario de Carlos del Amo - 23 de Abril de 2014 - 11:04
Buenos dias estoy haciendo el ejemplo que tienes y tras añadir las librerias que indicas me sale el siguiente error. Creo que falta alguna libreria, podrias indicarme cual es. Muchas gracias. El error es: Exception in thread "main" java.lang.NoClassDefFoundError: org/bouncycastle/asn1/DERConstructedSet at com.lowagie.text.pdf.PdfPKCS7.getEncodedPKCS7(Unknown Source) at com.lowagie.text.pdf.PdfPKCS7.getEncodedPKCS7(Unknown Source) at com.lowagie.text.pdf.PdfSigGenericPKCS.setSignInfo(Unknown Source) at com.lowagie.text.pdf.PdfSignatureAppearance.preClose(Unknown Source) at com.lowagie.text.pdf.PdfSignatureAppearance.preClose(Unknown Source) at com.lowagie.text.pdf.PdfStamper.close(Unknown Source) at test.TESTPDF02.main(TESTPDF02.java:53) Caused by: java.lang.ClassNotFoundException: org.bouncycastle.asn1.DERConstructedSet at java.net.URLClassLoader$1.run(URLClassLoader.java:366) at java.net.URLClassLoader$1.run(URLClassLoader.java:355) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:354) at java.lang.ClassLoader.loadClass(ClassLoader.java:425) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
Comentario de Alex - 13 de Mayo de 2014 - 22:51
Excelente post.. me ayudó bastante en una firma electrónica que estaba realizando pero no me creaba la firma en un .pdf pero usando tu método me lo hizo. Saludos.
Comentario de Dani - 27 de Mayo de 2014 - 19:32
Hola Juan , tengo una pregunta espero me puedas ayudar , como puedo listar los certificados que tengo en mi contenedor de certificados,ejemplo los que están en internet Explorer?? No se sí pudieras ayudarme, la idea es firmar mi PDF con mi certificado o el de mi mujer, poder elegir.
Comentario de Luis Ortiz - 05 de Junio de 2014 - 18:55
Hola Amigos tengo un problema con el código el error que me arroja es el siguiente : run: Exception in thread "main" java.lang.NoSuchMethodError: org.bouncycastle.asn1.DERSequence.<init>(Lorg/bouncycastle/asn1/DEREncodableVector;)V at com.lowagie.text.pdf.PdfPKCS7.getEncodedPKCS7(Unknown Source) at com.lowagie.text.pdf.PdfPKCS7.getEncodedPKCS7(Unknown Source) at com.lowagie.text.pdf.PdfSigGenericPKCS.setSignInfo(Unknown Source) at com.lowagie.text.pdf.PdfSignatureAppearance.preClose(Unknown Source) at com.lowagie.text.pdf.PdfSignatureAppearance.preClose(Unknown Source) at com.lowagie.text.pdf.PdfStamper.close(Unknown Source) at main.Main.main(Main.java:58) Estoy usando la version 2.1.7 de itext y he agregado las librerias bcprov-jdk16-146.jar, bctsp-jdk16-146.jar y bcmail-jdk16-146.jar
Comentario de Paco - 16 de Junio de 2014 - 03:16
Tengo el mismo problema que Luis Ortiz, tiene alguna solución?
Comentario de Percy - 23 de Septiembre de 2014 - 16:00
Me funcionó con los certificados de la reniec-peru, lo que me falta es validar que la clave introducida corresponda al certificado pfx, alguien sabe como se hace eso?
Comentario de Luis Orozco - 30 de Septiembre de 2014 - 22:49
Que tal compañeros, alguien sabe si podré realizar esté tipo de procedimiento para una app web utilizando php!? Obviamente debo utilizar un Applet para interactuar con el lector de tarjetas inteligentes, estoy utilizando un dispositivo Athena, puedo tener acceso al almacén de certificados pero no tengo idea de como generar el pdf con su respectivo recuadro para que sea visible al usuario final. Cualquier ayuda les estaré agradecido. Saludos.
Comentario de Annel Gonzalez - 21 de Abril de 2015 - 22:50
Hola,este post me a ayudado muchismo pero este paso no me quiere salir para validar la firma, espero que me puedan apoyar. Ya agregue mi certificado a la KeyStore o eso quiero pensar y cuando trato de ejecutar el proyecto PDFSigner me sale esto Necesito el nombre del PDF a firmarJava Result: 1 y termina el programa. No soy experta en JAVA no se que es, espero que me puedan apoyar. Muchas gracias.
Ha habido un error en el envío
Comentario enviado. Será revisado por la moderación antes de ser publicado.

Deja tu comentario

Tu nombre:
Tu email:
Tu comentario: