Cómo firmar digitalmente un documento PDF mediante Python. Además, generamos un certificado autofirmado propio (pfx, pem) y firmamos con este certificado generado. No necesitaremos un certificado expedido por una autoridad de certificación y no necesitaremos validar con autoridades de certificación externas. La aplicación permitirá insertar una firma en imagen en las coordenadas que indiquemos X,Y. Si el documento tiene varias páginas podremos elegir en cuales de ellas se insertará la imagen con la firma. También permitirá firmar varios documentos PDF a la vez, todos los que estén en una carpeta indicada.
- Instalar paquetes Python necesarios PDFNetPython3 y pyOpenSSL.
- Código fuente Python para generar certificado autofirmado y firmar documentos PDF.
- Ejecución de la aplicación Python para firmar documentos PDF con certificado autofirmado propio.
- Agregar certificado generado a identidades de confianza en Adobe Acrobat Reader.
- Descarga del fichero con el código fuente completo en Python para firmar documentos PDF.
Instalar paquetes Python necesarios PDFNetPython3 y pyOpenSSL
En primer lugar necesitaremos tener instalado Python en nuestro equipo. En el siguiente artículo explicamos cómo hacerlo:
Instalaremos los componentes (librerías, paquetes) que usaremos para la generación de un certificado autofirmado y para firmar el documento PDF. Para ello, desde la línea de comandos, ejecutaremos el siguiente comando:
pip install PDFNetPython3 pyOpenSSL
Este comando instalará en nuestro sistema las últimas versiones de PDFNetPython3 (en el momento de la realización de este artículo la 9.1.0) y pyOpenSSL (la versión 21.0.0), además, instalará otros paquetes dependientes: cryptography 36.0.0, pycparser 2.21 y cffi 1.15.0:
El paquete PDFNetPython3 dejó de ser gratuito en las últimas versiones. Por ello, para que funcione nuestra aplicación, tendremos que adquirir dicho paquete o bien usar una clave de prueba, que podemos obtener directamente (sin registro) desde su web oficial:
https://www.pdftron.com/pws/get-key
Copiaremos la clave de demostración y la pegaremos en la línea de código:
PDFNet.Initialize(«demo:1638008198625:7b6e5d3b030000000000ad6c0848ed2c55e035c1937ec70d40b850090b»)
(el código fuente lo veremos en el siguiente punto de este artículo)
Hay que tener en cuenta que PDFTron recopilará datos del uso de su SDK en nuestra aplicación Python y lo enviará a sus servidores. Así lo indica en la obtención de la clave de prueba (trial key):
PDFTron collects some data regarding your usage of the SDK for product improvement.
Si usamos la versión 8.1.0 de PDFTron (PDFNetPython3), no necesitaremos clave de licencia pues es gratuita, la instalaríamos con el comando:
pip install PDFNetPython3==8.1.0 pyOpenSSL==20.0.1
Y en la línea de código (que mostramos más adelante):
PDFNet.Initialize(«demo:1638008198625:7b6e5d3b030000000000ad6c0848ed2c55e035c1937ec70d40b850090b»)
La cambiaríamos por:
PDFNet.Initialize()
De esta forma usaríamos una versión gratuita y tampoco aparecería el mensaje:
PDFNet is running in demo mode.
Cada vez que ejecutemos nuestro programa.
Código fuente Python para generar certificado autofirmado y firmar documentos PDF
A continuación mostramos el código fuente completo de nuestra aplicación Python. Todas las líneas importantes de código van comentadas con su uso. También comentamos y explicamos para qué se usa cada método.
El script Python, que guardaremos con el nombre «firmar_pdf.py«, tendrá el siguiente código (se puede descargar aquí):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
import OpenSSL import os import time import argparse from PDFNetPython3.PDFNetPython import * from typing import Tuple """ Para crear un par de claves pública/privada Argumentos: tipo - Tipo de clave (TYPE_RSA o TYPE_DSA) bits - Números de bits para usar en la clave (1024, 2048 ó 4096) Devuelve: el par de claves pública/privada en un objeto PKey """ def CrearParDeClaves(tipo, bits): pkey = OpenSSL.crypto.PKey() pkey.generate_key(tipo, bits) return pkey """ Para crear un certificado autofirmado. Este certificado no requiere la firma de una autoridad de certificación externa. """ def CrearCertificadoAutofirmado(parDeClaves): # Crea un certificado autofirmado certificado = OpenSSL.crypto.X509() # Nombre Común para el certificado # Este dato aparecerá en el Emisor del certificado certificado.get_subject().CN = "Proyecto A" # Número de serie del certificado # Lo obtendremos de forma aleatoria, usando la hora actual y multiplicándola por 10 # Este dato aparecerá en el campo "Número de serie" del certificado certificado.set_serial_number(int(time.time() * 10)) # Inicio validez del certificado certificado.gmtime_adj_notBefore(0) # En la fecha actual # Fecha de expiración del certificado, le asignaremos 10 años certificado.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) # Campo Asunto del certificado # Asignaremos el mismo valor que el Emisor ("Proyecto A") certificado.set_issuer((certificado.get_subject())) certificado.set_pubkey(parDeClaves) certificado.sign(parDeClaves, 'md5') # también podría ser cert.sign(pKey, 'sha256') return certificado """ Para generar el certificado autofirmado """ def GenerarCertificado(): resumenAcciones = {} resumenAcciones['Versión OpenSSL'] = OpenSSL.__version__ # Generando Clave Privada para el certificado... Clave = CrearParDeClaves(OpenSSL.crypto.TYPE_RSA, 1024) # Codificado PEM # Generará un fichero .pem con la clave privada # Este fichero nunca debe ser revelado with open(os.path.dirname(os.path.abspath(__file__)) + '\static\private_key.pem', 'wb') as pk: pk_str = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, Clave) pk.write(pk_str) # Mostramos la clave privada sólo para depuración # En un programa en producción NUNCA debe mostrarse esta clave resumenAcciones['Clave Privada'] = pk_str # Generando una certificación de cliente autofirmada ... certificado = CrearCertificadoAutofirmado(parDeClaves=Clave) with open(os.path.dirname(os.path.abspath(__file__)) + '\static\certificate.cer', 'wb') as cer: cer_str = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, certificado) cer.write(cer_str) resumenAcciones['Certificado autofirmado'] = cer_str # Generando la clave pública ... with open(os.path.dirname(os.path.abspath(__file__)) + '\static\public_key.pem', 'wb') as pub_key: pub_key_str = OpenSSL.crypto.dump_publickey(OpenSSL.crypto.FILETYPE_PEM, certificado.get_pubkey()) pub_key.write(pub_key_str) resumenAcciones['Clave pública'] = pub_key_str # Take a private key and a certificate and combine them into a PKCS12 file. # Generating a container file of the private key and the certificate... # Usa la clave privada y el certificado y los combina en un archivo PKCS12 # Generando un archivo contenedor de la clave privada y el certificado ... p12 = OpenSSL.crypto.PKCS12() p12.set_privatekey(Clave) p12.set_certificate(certificado) # El archivo PKSC12 (.pfx) puede convertirse a formato PEM open(os.path.dirname(os.path.abspath(__file__)) + '\static\container.pfx', 'wb').write(p12.export()) # Mostrar el resumen de acciones print("*******************") print("\n".join("{}:{}".format(i, j) for i, j in resumenAcciones.items())) print("*******************") return True """ Firma un documento PDF """ def FirmarPDF(ficheroPDFEntrada: str, idFirma: str, coordenadaX: int, coordenadaY: int, paginas: Tuple = None, ficheroPDFFirmado: str = None): # Se genera automáticamente un archivo de salida con el final del nombre _firmado.df if not ficheroPDFFirmado: ficheroPDFFirmado = (os.path.splitext(ficheroPDFEntrada)[0]) + "_firmado.pdf" # Inicializamos la librería de edición de ficheros PDF # Introducimos la clave de licencia como parámetro PDFNet.Initialize("demo:1638008198625:7b6e5d3b030000000000ad6c0848ed2c55e035c1937ec70d40b850090b") doc = PDFDoc(ficheroPDFEntrada) # Creamos un campo de firma (imagen de firma escaneada) campoFirma = SignatureWidget.Create(doc, Rect( coordenadaX, coordenadaY, coordenadaX + 100, coordenadaY + 50), idFirma) # Recorremos las páginas indicadas para agregar la firma (en imagen) for pagina in range(1, (doc.GetPageCount() + 1)): if paginas: if str(pagina) not in paginas: continue pg = doc.GetPage(pagina) # Agrega el campo de firma en la página pg.AnnotPushBack(campoFirma) # Fichero con la imagen de la firma escaneada imagenFirma = os.path.dirname(os.path.abspath(__file__)) + "\static\img_firma.jpg" # Fichero PFX del certificado autofirmado ficheroPFX = os.path.dirname(os.path.abspath(__file__)) + "\static\container.pfx" # Recupera el campo de firma campoAprobacion = doc.GetField(idFirma) campoAprobacionFirma = DigitalSignatureField(campoAprobacion) # Agregamos la apariencia al campo de imagen de firma escaneada img = Image.Create(doc.GetSDFDoc(), imagenFirma) widgetAprobacionFirmaEncontrado = SignatureWidget(campoAprobacion.GetSDFObj()) widgetAprobacionFirmaEncontrado.CreateSignatureAppearance(img) # Preparamos la firma y el controlador de firma para firmar el documento PDF # print (ficheroPFX) campoAprobacionFirma.SignOnNextSave(ficheroPFX, '') # La firma se realizará durante la siguiente operación de guardado incremental doc.Save(ficheroPDFFirmado, SDFDoc.e_incremental) # Anotamos las acciones en el resumen resumenAcciones = { "PDF original": ficheroPDFEntrada, "ID de firma": idFirma, "PDF firmado": ficheroPDFFirmado, "Fichero de firma": imagenFirma, "Fichero de certificado": ficheroPFX } # Mostramos el resumen de acciones print("*******************") print("\n".join("{}:{}".format(i, j) for i, j in resumenAcciones.items())) print("*******************") return True """ Para firmar todos los documentos PDF contenidos en una carpeta (y subcarpetas) """ def FirmarPDFCarpeta(**kwargs): carpetaConPDF = kwargs.get('carpetaConPDF') idFirma = kwargs.get('idFirma') paginas = kwargs.get('paginas') coordenadaX = int(kwargs.get('coordenadaX')) coordenadaY = int(kwargs.get('coordenadaY')) # Ejecutamos en modo recursivo (carpeta y subcarpetas) recursivo = kwargs.get('recursivo') # Recorremos todos los ficheros de la carpeta indicada for carpeta, dirs, ficherosPDF in os.walk(carpetaConPDF): for ficheroPDF in ficherosPDF: # Comprobamos si es un fichero PDF (extensión .pdf) if not ficheroPDF.endswith('.pdf'): continue ficheroPDFEntrada = os.path.join(carpeta, ficheroPDF) print("Firmando PDF ", ficheroPDFEntrada) # Firmar PDF encontrado FirmarPDF(ficheroPDFEntrada=ficheroPDFEntrada, idFirma=idFirma, coordenadaX=coordenadaX, coordenadaY=coordenadaY, paginas=paginas, ficheroPDFFirmado=None) if not recursivo: break """ Verifica la ruta introducida y devuelve si es un fichero o una carpeta Se usará para comprobar si se firma un PDF o si se firman todos los PDF de una carpeta """ def VerificarRuta(ruta): if not ruta: raise ValueError(f"Ruta no válida") if os.path.isfile(ruta): return ruta elif os.path.isdir(ruta): return ruta else: raise ValueError(f"Ruta no válida {ruta}") """ Obtiene los parámetros de la línea de comandos que haya pasado el usuario """ def parse_args(): parser = argparse.ArgumentParser(description="Opciones disponibles") parser.add_argument('-g', '--GenerarCertificado', dest='GenerarCertificado', action="store_true", help="Carga la configuración y genera el certificado autofirmado") parser.add_argument('-i', '--carpetaPDF', dest='carpetaPDF', type=VerificarRuta, help="Fichero PDF o carpeta que contenga los PDF") parser.add_argument('-f', '--IDFirma', dest='idFirma', type=str, help="ID de la firma (Nombre del firmante)") parser.add_argument('-p', '--paginas', dest='paginas', type=tuple, help="Páginas en las que se incluirá la firma, ejemplo: [1,5,6]") parser.add_argument('-x', '--coordenadaX', dest='coordenadaX', type=int, help="Coordenada X en la que se incluirá la imagen de la firma.") parser.add_argument('-y', '--coordenadaY', dest='coordenadaY', type=int, help="Coordenada Y en la que se incluirá la imagen de la firma.") path = parser.parse_known_args()[0].carpetaPDF if path and os.path.isfile(path): parser.add_argument('-o', '--ficheroPDFFirmado', dest='ficheroPDFFirmado', type=str, help="Fichero PDF de salida firmado") if path and os.path.isdir(path): parser.add_argument('-r', '--recursivo', dest='recursivo', default=False, type=lambda x: ( str(x).lower() in ['true', '1', 'yes']), help="Firmar todos los ficheros PDF de una carpeta y subcarpetas (recursivo)") args = vars(parser.parse_args()) # Para mostrar los argumentos de la línea de comandos print("***** Argumentos *****") print("\n".join("{}:{}".format(i, j) for i, j in args.items())) print("**********************") return args """ Rutina principal del programa """ if __name__ == '__main__': # Comprobamos los argumentos pasados por la línea de comandos args = parse_args() if args['GenerarCertificado'] == True: GenerarCertificado() else: # Si es un archivo PDF if os.path.isfile(args['carpetaPDF']): FirmarPDF( ficheroPDFEntrada=args['carpetaPDF'], idFirma=args['idFirma'], coordenadaX=int(args['coordenadaX']), coordenadaY=int(args['coordenadaY']), paginas=args['paginas'], ficheroPDFFirmado=args['ficheroPDFFirmado'] ) # Si es una carpeta entera con ficheros PDF elif os.path.isdir(args['carpetaPDF']): # Procesamos todos los ficheros PDF de la carpeta para firmarlos todos # Si se ha indicado que sea recursivo firmará también los PDF de las subcarpetas FirmarPDFCarpeta( carpetaConPDF=args['carpetaPDF'], idFirma=args['idFirma'], coordenadaX=int(args['coordenadaX']), coordenadaY=int(args['coordenadaY']), paginas=args['paginas'], recursivo=args['recursivo'] ) |
En el código hemos incluido como Nombre Común del certificado «Proyecto A». Este dato aparecerá en los detalles del certificado al consultar las firmas del documento PDF, en el campo «Emisor«. Además, el campo «Número de serie» también lo generamos desde el código Python, de forma aleatoria en función de la hora actual del sistema (multiplicándola por 10). Estableceremos la validez del certificado a 10 años:
Ejecución de la aplicación Python para firmar documentos PDF con certificado autofirmado propio
Explicamos ahora cómo funciona la aplicación Python. La aplicación admite varios parámetros, que podemos mostrar ejecutando:
«C:/Program Files/python.exe» d:/ProyectoA_Python/PDF/Firmar/firmar_pdf.py –help
(suponemos que tenemos instalado python.exe en la carpeta C:/Program Files/python.exe y que tenemos el script python en d:/ProyectoA_Python/PDF/Firmar/ )
Los parámetros posibles son:
- -h, –help: muestra la pantalla de ayuda con los parémetros posibles y su uso.
- -g, –GenerarCertificado: genera un certificado autofirmado en la carpeta «static» de la ruta donde se encuentre el script (esta carpeta «static» debe estar creada previamente). Generará los ficheros .cer, .pem (clave pública y clave privada) y .pfx.
- -i CARPETAPDF, –carpetaPDF CARPETAPDF: fichero PDF que se firmará. Si se indica sólo una carpeta, la aplicación firmará todos los documentos PDF que existan en esa carpeta. Permite firmar o bien un único fichero PDF o bien todos los PDF de una carpeta y subcarpeta indicadas.
- -f IDFIRMA, –IDFirma IDFIRMA: nombre del firmate.
- -p PAGINAS, –paginas PAGINAS: páginas del fichero (o ficheros) PDF en las que se agregará la imagen de la firma escaneada (cogida del fichero img_firma.jpg). Indicaremos las páginas con el formato [1,2,3,4]. Por ejemplo, si el documento PDF tiene 4 páginas y queremos incluir la imagen de la firma en la página 1 y la página cuatro, agregaremos el parámetro -p [1,4]
- -x COORDENADAX, –coordenadaX COORDENADAX: coordenada X de la posición en la que se incluirá la imagen de la firma.
- -y COORDENADAY, –coordenadaY COORDENADAY: coordenada Y de la posición en la que se incluirá la imagen de la firma.
- -r true/false, –recursivo true/false: si se indica una carpeta en lugar de un fichero PDF, y se indica este parámetro a true, firmará todos los documentos PDF de todas las subcarpetas contenidas en la carpeta indicada.
En primer lugar crearemos la carpeta «static» en la carpeta donde tengamos el fichero de script Python:
A continuación generaremos el certificado autofirmado (no requiere de entidad certificadora externa) ejecutando el comando:
«C:/Program Files/python.exe» d:/ProyectoA_Python/PDF/Firmar/firmar_pdf.py –g
El comando anterior habrá generado el fichero de certificado certificate.cer, el contenedor container.pfx y los ficheros PEM de la clave pública public_key.pem y la clave privada private_key.pem:
Por otro lado, si queremos que se inserte una imagen con la firma escaneada, colocaremos el fichero de la imagen con formato jpg, con el nombre img_firma.jpg, en la carpeta static:
A partir de ahora podremos firmar documentos PDF con este certificado generado. Por ejemplo, para firmar el documento PDF «documento_ejemplo_1_pagina.pdf» (con una sola página), ubicado en «D:\ProyectoA_Python\PDF\Firmar\PDF«, insertándole el fichero de imagen de firma en la posición (330,100), y el fichero firmado PDF con el nombre «documento_ejemplo_1_pagina_firmado.pdf«, usaremos el comando:
«C:/Program Files/python.exe» d:/ProyectoA_Python/PDF/Firmar/firmar_pdf.py -i «D:\ProyectoA_Python\PDF\Firmar\PDF\documento_ejemplo_1_pagina.pdf» -f «ProyectoA» -x 330 -y 100 -o «D:\ProyectoA_Python\PDF\Firmar\PDF\documento_ejemplo_1_pagina_firmado.pdf»
Nos habrá generado el documento documento_ejemplo_1_pagina_firmado.pdf , copia del documento documento_ejemplo_1_pagina.pdf, pero con la firma digital (usando nuestro certificado autofirmado) y con la firma en imagen:
Como vemos, al abrirlo nos mostrará un mensaje indicando «Hay al menos una firma que presenta problemas». El motivo de esta advertencia es que Acrobat Reader intenta validar el certificado en una identidad de confianza de Acrobat Reader. En este punto explicamos cómo evitar este mensaje. El certificado generado es un certificado «normal» y pueden consultarse sus datos pulsando en «Panel de firma» o pulsando en la imagen de firma escaneada. Pulsaremos en «Propiedades de la firma…»:
Y pulsaremos en «Mostrar certificado del firmante…»:
Nos mostrará los datos del certificado:
Para firmar todos los documentos PDF contenidos en una carpeta podemos ejecutar el comando:
«C:/Program Files/python.exe» d:/ProyectoA_Python/PDF/Firmar/firmar_pdf.py -i «D:\ProyectoA_Python\PDF\Firmar\PDF\» -f «ProyectoA» -x 330 -y 100 -p [1,7] -r true
Firmará todos los documentos PDF que haya en la carpeta D:\ProyectoA_Python\PDF\Firmar\PDF\, si tienen varias páginas incluirá la firma de imagen en las páginas 1 y 7. Y al indicar el parámetro -r true, será recursivo y firmará todos los PDF que haya en las subcarpetas de la carpeta indicada:
El comando anterior generará un fichero duplicado de cada PDF, añadiendo al nombre …_firmado.pdf con el PDF firmado:
Agregar certificado generado a identidades de confianza en Adobe Acrobat Reader
Cuando abrimos el documento PDF firmado, nos aparecerá un mensaje en el panel de firma indicando: Hay al menos una firma que presenta problemas. Este mensaje no significa que la firma digital no sea válida, indica que Adobe Acrobat Reader no puede validar automáticamente la firma digital agregada (con certificado autofirmado) porque el certificado no está en la lista de identidades de confianza.
Si queremos evitar este mensaje de aviso, podremos agregar nuestro certificado autofirmado a las identidades de confianza. Para ello abriremos Adobe Acrobat Reader, pulsaremos en el menú «Edición» – «Preferencias»:
Seleccionaremos la categoría «Firmas» a la izquierda, y a la derecha pulsaremos en «Más…» en el grupo «Identidades y certificados de confianza»:
Pulsaremos en «Certificados de confianza» en la izquierda y en la barra de botones pulsaremos en «Importar»:
Elegiremos el fichero de certificado autofirmado generado con nuestra aplicación Python certificate.cer (de la carpeta static):
Pulsaremos en «Importar»:
Nos indicará que se ha importado un certificado emisor. Pulsaremos «Aceptar»:
Seleccionaremos el certificado importado (en nuestro caso «Proyecto A») y pulsaremos en el botón «Editar confianza»:
Marcaremos el check «Utilizar este certificado como raíz de confianza» y pulsaremos «Aceptar»:
A partir de ahora Acrobat Reader considerará esta identidad como de confianza y el panel de firma lo mostrará en verde con el mensaje «Firmado y todas las firmas son válidas»:
Si pulsamos en el panel de firma o en la propia imagen de firma ahora nos mostrará un mensaje indicando: La firma es VÁLIDA, firmada por Proyecto A. No ha havido modificaciones en: documento desde que se firmó. La identidad del firmaten es válida.
Descarga del fichero con el código fuente completo en Python para firmar documentos PDF
La descarga del fichero firmar_pdf.py con el código fuente completo de la aplicación que genera certificados autofirmados y firma digitalmente con este certificado documentos PDF: