python

Python – Firmar PDF con P12 y Flask

En este post te compartiré los resultados de mi investigación sobre cómo firmar, digitalmente, un documento PDF usando un certificado con extensión P12 y la librería endesive.

Al final terminé creando una aplicación web con Flask que permite que elijas el certificado, el PDF y la contraseña para devolverte el PDF pero firmado digitalmente.

Ten en cuenta que yo probé con un certificado de prueba generado por mí mismo, así que no sé si esto funcione con algo real.

Firmando PDF

En el repositorio oficial de endesive ya está un ejemplo de cómo firmar un PDF. Yo modifiqué la función para que devolviera los bytes del PDF firmado en lugar de guardarlo en el almacenamiento local ya que los necesito para enviarlos con Flask.

Al final separé todo en un archivo llamado firmador.py y se ve así:

# *-* coding: utf-8 *-*
import datetime
from cryptography.hazmat import backends
from cryptography.hazmat.primitives.serialization import pkcs12

from endesive.pdf import cms


def firmar(contraseña, certificado, pdf):
    date = datetime.datetime.utcnow() - datetime.timedelta(hours=12)
    date = date.strftime("D:%Y%m%d%H%M%S+00'00'")
    dct = {
        "aligned": 0,
        "sigflags": 3,
        "sigflagsft": 132,
        "sigpage": 0,
        "sigbutton": True,
        "sigfield": "Signature1",
        "auto_sigfield": True,
        "sigandcertify": True,
        "signaturebox": (470, 840, 570, 640),
        "signature": "Aquí va la firma",
        # "signature_img": "signature_test.png",
        "contact": "hola@ejemplo.com",
        "location": "Ubicación",
        "signingdate": date,
        "reason": "Razón",
        "password": contraseña,
    }
    # with open("cert.p12", "rb") as fp:
    p12 = pkcs12.load_key_and_certificates(
        certificado.read(), contraseña.encode("ascii"), backends.default_backend()
    )

    #datau = open(fname, "rb").read()
    datau = pdf.read()
    datas = cms.sign(datau, dct, p12[0], p12[1], p12[2], "sha256")
    return datau, datas
    """
    fname = "test.pdf"
    with open(fname, "wb") as fp:
        fp.write(datau)
        fp.write(datas)
    """

Y sí, solo comenté las partes que no me servían. No eliminé nada por si necesitaba volver a lo anterior… ya sabes, cosas de programadores.

Una cosa que quiero contarte es que si especificas signature_img y no especificas signature entonces se va a firmar usando una imagen. Esa imagen puede ser una foto de la firma manuscrita real, solo para que se vea más bonito.

Ahora simplemente invoco esa función desde Flask como verás a continuación.

Formulario para firmar PDF con Python

Formulario para firmar PDF con Python – Seleccionar P12, contraseña y archivo

Antes de continuar veamos el formulario. El mismo necesitará 2 archivos que serán el PDF y el P12, y un tipo texto (o mejor dicho, password):

<!DOCTYPE html>
<html lang="es">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Firmar - By Parzibyte</title>
    <link rel="stylesheet" href="https://unpkg.com/bulma@0.9.1/css/bulma.min.css">
</head>

<body>

    <nav class="navbar is-warning" role="navigation" aria-label="main navigation">
        <div class="navbar-brand">
            <a class="navbar-item" href="https://parzibyte.me/blog">
                <img alt=""
                    src="https://raw.githubusercontent.com/parzibyte/ejemplo-mern/master/src/img/parzibyte_logo.png"
                    style="max-height: 80px" />
            </a>
            <button class="navbar-burger is-warning button" aria-label="menu" aria-expanded="false"
                data-target="navbarBasicExample">
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
            </button>
        </div>
        <div class="navbar-menu">
            <div class="navbar-end">
                <div class="navbar-item">
                    <div class="buttons">
                        <a target="_blank" rel="noreferrer" href="https://parzibyte.me/blog" class="button is-primary">
                            <strong>By Parzibyte</strong>
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </nav>
    <script type="text/javascript">
        document.addEventListener("DOMContentLoaded", () => {
            const boton = document.querySelector(".navbar-burger");
            const menu = document.querySelector(".navbar-menu");
            boton.onclick = () => {
                menu.classList.toggle("is-active");
                boton.classList.toggle("is-active");
            };
        });
    </script>
    <section class="section">
        <div class="columns">
            <div class="column">
                <h1 class="is-size-1">Firmar documento</h1>
                <form method="POST" action="{{url_for('procesar')}}" enctype="multipart/form-data">
                    <div class="field">
                        <label class="label">Contraseña</label>
                        <div class="control">
                            <input class="input" name="palabra_secreta" required type="password"
                                placeholder="Contraseña">
                        </div>
                    </div>
                    <div class="field">
                        <div class="file has-name">
                            <label class="file-label">
                                <input class="file-input" type="file" name="pdf" required accept=".pdf">
                                <span class="file-cta">
                                    <span class="file-icon">
                                        <i class="fas fa-upload"></i>
                                    </span>
                                    <span class="file-label">
                                        Seleccione el PDF
                                    </span>
                                </span>
                                <span class="file-name" id="nombrePdf">
                                </span>
                            </label>
                        </div>
                    </div>
                    <div class="field">
                        <div class="file has-name">
                            <label class="file-label">
                                <input class="file-input" type="file" name="firma" required accept=".p12">
                                <span class="file-cta">
                                    <span class="file-icon">
                                        <i class="fas fa-upload"></i>
                                    </span>
                                    <span class="file-label">
                                        Seleccione la firma
                                    </span>
                                </span>
                                <span class="file-name" id="nombreFirma">
                                </span>
                            </label>
                        </div>
                    </div>
                    <input class="button is-primary" type="submit" value="Firmar">
                </form>
            </div>

        </div>
    </section>
    <script>
        document.addEventListener("DOMContentLoaded", () => {
            const $pdf = document.querySelector("[name='pdf']");
            const $nombrePdf = document.querySelector("#nombrePdf");
            const $firma = document.querySelector("[name='firma']");
            const $nombreFirma = document.querySelector("#nombreFirma");
            $pdf.onchange = () => {
                if ($pdf.files.length <= 0) {
                    $nombrePdf.textContent = "";
                    return;
                }
                const archivo = $pdf.files[0];
                $nombrePdf.textContent = archivo.name;
            };

            $firma.onchange = () => {
                if ($firma.files.length <= 0) {
                    $nombreFirma.textContent = "";
                    return;
                }
                const archivo = $firma.files[0];
                $nombreFirma.textContent = archivo.name;
            };
        });
    </script>
</body>

</html>

Es una plantilla que será servida con Flask en determinada ruta. El JavaScript que ves es para que se coloque el nombre del archivo seleccionado en el input, solo eso.

Firmando PDF con Python y Flask

Ahora veamos la configuración de Flask que es todo el servidor. Aquí aunque parezca simple tuve que investigar cómo leer el archivo sin almacenarlo temporalmente (ya que vamos a firmar algo, no debemos dejar la firma o cosas similares en el servidor).

import io
from flask import Flask, render_template, request, send_file
from firmador import firmar

app = Flask(__name__)
app.config['TEMPLATES_AUTO_RELOAD'] = True


@app.route('/')
def index():
    return render_template("formulario.html")


@app.route('/procesar',  methods=['POST'])
def procesar():
    pdf = request.files.get("pdf")
    firma = request.files.get("firma")
    contraseña = request.form.get("palabra_secreta")
    archivo_pdf_para_enviar_al_cliente = io.BytesIO()
    try:
        datau, datas = firmar(contraseña, firma, pdf)
        archivo_pdf_para_enviar_al_cliente.write(datau)
        archivo_pdf_para_enviar_al_cliente.write(datas)
        archivo_pdf_para_enviar_al_cliente.seek(0)
        return send_file(archivo_pdf_para_enviar_al_cliente, mimetype="application/pdf",
                         download_name="firmado" + ".pdf",
                         as_attachment=True)
    except ValueError as e:
        return "Error firmando: " + str(e) + " . Se recomienda revisar la contraseña y el certificado"


if __name__ == "__main__":

    app.run(host='0.0.0.0', port=81)

La magia está en la línea 15. Ahí se va a procesar lo que el formulario envíe.

Extraemos la firma, la contraseña y el PDF. Luego invocamos a la función del firmador que nos va a devolver el PDF firmado como un montón de bytes (igual nosotros le enviamos la firma y el PDF original como muchos bytes).

Finalmente rebobinamos el archivo en la línea 24 y lo servimos directamente como un adjunto en el navegador.

Al final el usuario presionará el botón “Firmar” y unos segundos después se le estará preguntando en dónde guardar el PDF ya firmado.

Analizando documento PDF firmado

Si todo va bien entonces tendremos el PDF ya firmado con un certificado P12 o PKCS 12 usando endesive y Flask.

Al analizarlo con Adobe Acrobat Reader vemos que ya está firmado correctamente pero que la firma es inválida porque como ya dije anteriormente yo generé mi certificado para probar.

Analizando documento PDF firmado con P12

Poniendo todo junto

Te voy a dejar el código completo en mi GitHub. Dentro del mismo encontrarás un README con las instrucciones que igual colocaré aquí (aunque lo más actualizado estará en el repositorio):

Debe decir algo como:

* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on all addresses (0.0.0.0)
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:81
* Running on http://192.168.100.6:81 (Press CTRL+C to quit)

Navegar a http://localhost:81/ y usar la webapp

Si aparece algo como “python no se reconoce como un comando externo” o cosas así, revisar que se haya agregado correctamente a la PATH.

Finalmente te dejo con más tutoriales de Python en mi blog.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

3 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

3 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

3 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

3 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

3 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

4 días hace

Esta web usa cookies.