Browse Source

Fix(Assinatura): Corrige ordem de assinatura para validação ITI

- Adiciona carimbo visual ANTES de assinar digitalmente
- Garante que a assinatura protege o documento completo
- Resolve problema de assinatura corrompida no validador ITI
pull/3858/head
rangelbruno 8 months ago
parent
commit
405f5996a7
  1. 103
      sapl/materia/views_assinatura.py

103
sapl/materia/views_assinatura.py

@ -228,14 +228,20 @@ def materia_assinar_a1(request, pk):
'error': 'Certificado expirado ou ainda não válido.' 'error': 'Certificado expirado ou ainda não válido.'
}, status=400) }, status=400)
# Cria arquivo temporário para o PDF assinado # Cria arquivo temporário para o PDF com carimbo
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_signed: with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_stamped:
temp_signed_path = temp_signed.name temp_stamped_path = temp_stamped.name
try: try:
from pyhanko.sign.fields import SigSeedSubFilter from pyhanko.sign.fields import SigSeedSubFilter
from pyhanko.pdf_utils import text from pyhanko.pdf_utils import text
from pyhanko.sign.general import SigningError from pyhanko.sign.general import SigningError
from PyPDF4 import PdfFileReader, PdfFileWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
import re
# Informações do assinante # Informações do assinante
nome_assinante = request.user.get_full_name() or request.user.username nome_assinante = request.user.get_full_name() or request.user.username
@ -263,35 +269,6 @@ def materia_assinar_a1(request, pk):
else: else:
tipo_cert = "Certificado Digital" tipo_cert = "Certificado Digital"
# Assina o PDF com assinatura invisível (sem campo visual do pyhanko)
with io.BytesIO(pdf_bytes) as inf:
w = IncrementalPdfFileWriter(inf)
# Metadados da assinatura (sem campo visível)
meta = signers.PdfSignatureMetadata(
field_name='AssinaturaDigital',
location='Câmara Municipal',
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001',
name=nome_assinante
)
# Executa assinatura invisível
with open(temp_signed_path, 'wb') as outf:
signers.sign_pdf(
w,
meta,
signer=signer,
output=outf
)
# Adiciona carimbo de texto visível ao PDF assinado
# Usando reportlab para adicionar o texto
from PyPDF4 import PdfFileReader, PdfFileWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
# Tenta obter CPF do parlamentar ou do certificado # Tenta obter CPF do parlamentar ou do certificado
cpf = "" cpf = ""
try: try:
@ -303,7 +280,6 @@ def materia_assinar_a1(request, pk):
# Se não encontrou CPF no parlamentar, tenta extrair do certificado # Se não encontrou CPF no parlamentar, tenta extrair do certificado
if not cpf: if not cpf:
subject_str = str(cert_info.subject) subject_str = str(cert_info.subject)
import re
cpf_match = re.search(r'\d{3}\.?\d{3}\.?\d{3}-?\d{2}', subject_str) cpf_match = re.search(r'\d{3}\.?\d{3}\.?\d{3}-?\d{2}', subject_str)
if cpf_match: if cpf_match:
cpf = cpf_match.group() cpf = cpf_match.group()
@ -311,6 +287,8 @@ def materia_assinar_a1(request, pk):
# Formata data # Formata data
data_simples = data_assinatura.strftime('%d/%m/%Y %H:%M') data_simples = data_assinatura.strftime('%d/%m/%Y %H:%M')
# ===== PASSO 1: Criar carimbo visual e adicionar ao PDF ANTES de assinar =====
# Cria PDF com o carimbo de assinatura # Cria PDF com o carimbo de assinatura
stamp_buffer = io.BytesIO() stamp_buffer = io.BytesIO()
c = canvas.Canvas(stamp_buffer, pagesize=A4) c = canvas.Canvas(stamp_buffer, pagesize=A4)
@ -318,7 +296,7 @@ def materia_assinar_a1(request, pk):
# Posição do carimbo (canto inferior esquerdo) # Posição do carimbo (canto inferior esquerdo)
y_pos = 15 * mm y_pos = 15 * mm
x_pos = 10 * mm x_pos = 10 * mm
largura_carimbo = 70 * mm # Aumentado para caber texto + logo largura_carimbo = 70 * mm
altura_carimbo = 22 * mm altura_carimbo = 22 * mm
logo_width = 18 * mm logo_width = 18 * mm
@ -334,7 +312,6 @@ def materia_assinar_a1(request, pk):
c.setFont("Helvetica-Bold", 7) c.setFont("Helvetica-Bold", 7)
c.setFillColorRGB(0, 0, 0) c.setFillColorRGB(0, 0, 0)
# Nome em maiúsculas, quebra se muito longo
nome_upper = nome_assinante.upper() nome_upper = nome_assinante.upper()
if len(nome_upper) > 28: if len(nome_upper) > 28:
nome_upper = nome_upper[:28] + "..." nome_upper = nome_upper[:28] + "..."
@ -350,21 +327,17 @@ def materia_assinar_a1(request, pk):
# Logo da câmara (lado direito) # Logo da câmara (lado direito)
try: try:
# Procura o logotipo da câmara em vários locais possíveis
logo_path = None logo_path = None
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
possible_paths = [ possible_paths = [
# Logo padrão do sistema
os.path.join(base_dir, 'sapl/static/sapl/frontend/img/logo-camara-padrao.png'), os.path.join(base_dir, 'sapl/static/sapl/frontend/img/logo-camara-padrao.png'),
os.path.join(base_dir, 'sapl/static/sapl/frontend/img/logo.png'), os.path.join(base_dir, 'sapl/static/sapl/frontend/img/logo.png'),
os.path.join(base_dir, 'sapl/static/sapl/frontend/img/pdflogo.png'), os.path.join(base_dir, 'sapl/static/sapl/frontend/img/pdflogo.png'),
# Media folder
os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo/logo.png'), os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo/logo.png'),
os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo/logotipo.png'), os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo/logotipo.png'),
] ]
# Procura qualquer imagem na pasta de logotipo do media
logo_dir = os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo') logo_dir = os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo')
if os.path.exists(logo_dir): if os.path.exists(logo_dir):
for f in os.listdir(logo_dir): for f in os.listdir(logo_dir):
@ -377,7 +350,6 @@ def materia_assinar_a1(request, pk):
break break
if logo_path: if logo_path:
# Desenha logo no lado direito do carimbo
logo = ImageReader(logo_path) logo = ImageReader(logo_path)
logo_x = x_pos + largura_carimbo - logo_width - 2*mm logo_x = x_pos + largura_carimbo - logo_width - 2*mm
logo_y = y_pos + 2*mm logo_y = y_pos + 2*mm
@ -390,23 +362,48 @@ def materia_assinar_a1(request, pk):
c.save() c.save()
stamp_buffer.seek(0) stamp_buffer.seek(0)
# Mescla o carimbo com o PDF assinado # Mescla o carimbo com o PDF original (ANTES de assinar)
stamp_pdf = PdfFileReader(stamp_buffer) stamp_pdf = PdfFileReader(stamp_buffer)
signed_pdf = PdfFileReader(open(temp_signed_path, 'rb')) original_pdf = PdfFileReader(io.BytesIO(pdf_bytes))
output_pdf = PdfFileWriter() output_pdf = PdfFileWriter()
# Adiciona o carimbo em todas as páginas for page_num in range(original_pdf.getNumPages()):
for page_num in range(signed_pdf.getNumPages()): page = original_pdf.getPage(page_num)
page = signed_pdf.getPage(page_num) if page_num == original_pdf.getNumPages() - 1: # Última página
if page_num == signed_pdf.getNumPages() - 1: # Última página
page.mergePage(stamp_pdf.getPage(0)) page.mergePage(stamp_pdf.getPage(0))
output_pdf.addPage(page) output_pdf.addPage(page)
# Salva o PDF final com carimbo # Salva o PDF com carimbo em arquivo temporário
final_buffer = io.BytesIO() with open(temp_stamped_path, 'wb') as f:
output_pdf.write(final_buffer) output_pdf.write(f)
final_buffer.seek(0)
signed_pdf_content = final_buffer.read() # ===== PASSO 2: Assinar o PDF que já contém o carimbo =====
with open(temp_stamped_path, 'rb') as stamped_file:
stamped_bytes = stamped_file.read()
signed_buffer = io.BytesIO()
with io.BytesIO(stamped_bytes) as inf:
w = IncrementalPdfFileWriter(inf)
# Metadados da assinatura
meta = signers.PdfSignatureMetadata(
field_name='AssinaturaDigital',
location='Câmara Municipal',
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001',
name=nome_assinante
)
# Executa assinatura
signers.sign_pdf(
w,
meta,
signer=signer,
output=signed_buffer
)
signed_buffer.seek(0)
signed_pdf_content = signed_buffer.read()
# Salva o PDF assinado no modelo # Salva o PDF assinado no modelo
filename = f"materia_{materia.pk}_assinado_{int(timezone.now().timestamp())}.pdf" filename = f"materia_{materia.pk}_assinado_{int(timezone.now().timestamp())}.pdf"
@ -444,8 +441,8 @@ def materia_assinar_a1(request, pk):
finally: finally:
# Remove arquivo temporário # Remove arquivo temporário
if os.path.exists(temp_signed_path): if os.path.exists(temp_stamped_path):
os.unlink(temp_signed_path) os.unlink(temp_stamped_path)
except ImportError: except ImportError:
logger.error("pyhanko não está instalado") logger.error("pyhanko não está instalado")

Loading…
Cancel
Save