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.
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.
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.
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.
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.
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):
pip install Flask
pip install endesive
python server.py
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.
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.