2026-01-22 10:37:35 +00:00
|
|
|
|
import base64
|
|
|
|
|
|
import io
|
2026-01-30 13:12:05 +00:00
|
|
|
|
from datetime import datetime, timezone
|
2026-01-22 10:37:35 +00:00
|
|
|
|
|
2026-01-30 13:12:05 +00:00
|
|
|
|
from pyhanko import stamp
|
|
|
|
|
|
from pyhanko.sign import fields, signers
|
2026-01-22 10:37:35 +00:00
|
|
|
|
from pyhanko.sign import signers
|
2026-01-30 13:12:05 +00:00
|
|
|
|
|
|
|
|
|
|
from pyhanko.pdf_utils import text
|
2026-01-29 17:35:43 +00:00
|
|
|
|
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
|
2026-01-22 10:37:35 +00:00
|
|
|
|
|
2026-01-30 13:12:05 +00:00
|
|
|
|
|
2026-01-22 10:37:35 +00:00
|
|
|
|
from signing_service.domain.ports.pdf_signer import PDFSignerPort
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PyHankoPDFSigner(PDFSignerPort):
|
2026-01-29 17:35:43 +00:00
|
|
|
|
async def sign(self, pdf_bytes: bytes, certificate: str, password: str,) -> bytes:
|
2026-01-22 10:37:35 +00:00
|
|
|
|
"""
|
2026-01-29 17:35:43 +00:00
|
|
|
|
pdf_bytes: original PDF byte
|
2026-01-22 10:37:35 +00:00
|
|
|
|
certificate: base64-encoded PKCS#12 (PFX)
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# 1️⃣ Decodificar certificado
|
|
|
|
|
|
pfx_bytes = base64.b64decode(certificate)
|
|
|
|
|
|
|
2026-01-30 13:12:05 +00:00
|
|
|
|
# 3️⃣ Preparar PDF en modo incremental
|
|
|
|
|
|
writer = IncrementalPdfFileWriter(io.BytesIO(pdf_bytes))
|
|
|
|
|
|
|
2026-01-22 10:37:35 +00:00
|
|
|
|
# 2️⃣ Cargar clave y certificado
|
2026-01-29 17:35:43 +00:00
|
|
|
|
signer = signers.SimpleSigner.load_pkcs12_data(
|
|
|
|
|
|
pkcs12_bytes=pfx_bytes,
|
|
|
|
|
|
passphrase=password.encode(),
|
|
|
|
|
|
other_certs=[],
|
2026-01-22 10:37:35 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-30 13:12:05 +00:00
|
|
|
|
# 3️⃣ Extract certificate info (from signer)
|
|
|
|
|
|
cert = signer.signing_cert
|
|
|
|
|
|
subject = cert.subject.native.get('common_name', 'Desconocido')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 5️⃣ Define signature field position
|
|
|
|
|
|
fields.append_signature_field(
|
|
|
|
|
|
writer,
|
|
|
|
|
|
sig_field_spec=fields.SigFieldSpec(
|
|
|
|
|
|
sig_field_name="Signature1",
|
|
|
|
|
|
on_page=-1,
|
|
|
|
|
|
box=(380, 100, 560, 160), # x, y, x+width, y+height
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 4️⃣ Signature metadata (panel de firmas)
|
|
|
|
|
|
meta = signers.PdfSignatureMetadata(
|
|
|
|
|
|
field_name="Signature1",
|
|
|
|
|
|
name=subject,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-22 10:37:35 +00:00
|
|
|
|
|
2026-01-30 13:12:05 +00:00
|
|
|
|
pdf_signer = signers.PdfSigner(
|
|
|
|
|
|
meta, signer=signer, stamp_style=stamp.TextStampStyle(
|
|
|
|
|
|
stamp_text='Firmado digitalmente por: %(signer)s\nFecha: %(ts)s',
|
|
|
|
|
|
text_box_style=text.TextBoxStyle(
|
|
|
|
|
|
border_width=0,
|
|
|
|
|
|
font_size=12
|
|
|
|
|
|
#font=opentype.GlyphAccumulatorFactory('path to font police.ttf'),
|
|
|
|
|
|
),
|
|
|
|
|
|
#background=images.PdfImage('path to img.jpg')
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-22 10:37:35 +00:00
|
|
|
|
# 4️⃣ Firmar
|
2026-01-30 13:12:05 +00:00
|
|
|
|
signed_pdf: io.BytesIO = await pdf_signer.async_sign_pdf(
|
|
|
|
|
|
writer,
|
2026-01-22 10:37:35 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-29 17:35:43 +00:00
|
|
|
|
# 5️⃣ Obtener PDF firmado
|
|
|
|
|
|
return signed_pdf.getbuffer().tobytes()
|