#!/usr/bin/env python3 """ Digital Signature Verifier & Stamper (ЭЦП ГОСТ Р 7.0.97-2016) Для проекта «Домовой 2.0». Распознает открепленные подписи (.sig, .p7s, .sgn), извлекает данные сертификата и накладывает официальный штамп верификации на целевой PDF-документ. """ import os import sys import subprocess import tempfile import logging import fitz # PyMuPDF from pathlib import Path from datetime import datetime log = logging.getLogger("SigVerifier") def extract_cert_info_openssl(sig_bytes): """ Извлекает информацию о сертификате из PKCS#7/CMS подписи через openssl. Поддерживает как ГОСТ, так и RSA сертификаты госорганов. """ info = { "valid": False, "serial": "Не определен", "owner": "Не определен", "org": "Государственный орган", "valid_from": "-", "valid_to": "-", "issuer": "Удостоверяющий центр" } with tempfile.NamedTemporaryFile(suffix=".sig", delete=False) as f: f.write(sig_bytes) sig_path = f.name try: # Пробуем через pkcs7 cmd = ["openssl", "pkcs7", "-inform", "DER", "-in", sig_path, "-print_certs", "-text"] res = subprocess.run(cmd, capture_output=True, text=True, errors="replace") # Если DER не подошел, пробуем PEM if res.returncode != 0: cmd = ["openssl", "pkcs7", "-inform", "PEM", "-in", sig_path, "-print_certs", "-text"] res = subprocess.run(cmd, capture_output=True, text=True, errors="replace") # Если pkcs7 не открыл, пробуем cms if res.returncode != 0 or not res.stdout.strip(): cmd = ["openssl", "cms", "-inform", "DER", "-in", sig_path, "-cmsout", "-print_certs", "-text"] res = subprocess.run(cmd, capture_output=True, text=True, errors="replace") output = res.stdout if output: info["valid"] = True # Парсим Subject for line in output.splitlines(): line_str = line.strip() if "Serial Number:" in line_str or "serialNumber" in line_str: parts = line_str.split(":", 1) if len(parts) > 1 and len(parts[1].strip()) > 4: info["serial"] = parts[1].strip() elif "Subject:" in line_str: # Извлекаем CN, O, OU if "CN =" in line_str or "CN=" in line_str: cn = line_str.split("CN", 1)[1].split(",")[0].replace("=", "").strip() info["owner"] = cn if "O =" in line_str or "O=" in line_str: o = line_str.split("O", 1)[1].split(",")[0].replace("=", "").strip() info["org"] = o elif "Issuer:" in line_str: if "CN =" in line_str or "CN=" in line_str: iss = line_str.split("CN", 1)[1].split(",")[0].replace("=", "").strip() info["issuer"] = iss elif "Not Before:" in line_str: info["valid_from"] = line_str.replace("Not Before:", "").strip() elif "Not After :" in line_str or "Not After:" in line_str: info["valid_to"] = line_str.split(":", 1)[1].strip() except Exception as e: log.error(f"Error parsing signature: {e}") finally: if os.path.exists(sig_path): os.unlink(sig_path) return info def stamp_pdf_with_signature(pdf_bytes, sig_info): """ Впечатывает официальный штамп ЭЦП в правый нижний угол первой страницы PDF. Если на первой странице плотный текст, штамп добавляется с полупрозрачной подложкой. """ try: doc = fitz.open(stream=pdf_bytes, filetype="pdf") if len(doc) == 0: return pdf_bytes page = doc[0] rect = page.rect # Габариты штампа (ГОСТ: ширина ~60-70 мм = ~170-200 pt, высота ~70-80 pt) stamp_w = 260 stamp_h = 75 margin = 20 # Размещаем в правом нижнем углу x0 = rect.width - stamp_w - margin y0 = rect.height - stamp_h - margin x1 = rect.width - margin y1 = rect.height - margin stamp_rect = fitz.Rect(x0, y0, x1, y1) # 1. Белая подложка с синей рамкой page.draw_rect(stamp_rect, color=(0.1, 0.3, 0.7), fill=(0.97, 0.98, 1.0), width=1.5) # 2. Текст штампа (Lissyara UNIX-Minimalism / ГОСТ стиль) owner_name = sig_info.get("owner", "Должностное лицо") org_name = sig_info.get("org", "Орган государственной власти") serial = sig_info.get("serial", "00E1...") valid_period = f"{sig_info.get('valid_from', '-')} — {sig_info.get('valid_to', '-')}" text = ( "ДОКУМЕНТ ПОДПИСАН ЭЛЕКТРОННОЙ ПОДПИСЬЮ\n" f"Владелец: {owner_name}\n" f"Орган: {org_name[:40]}\n" f"Сертификат: {serial[:28]}\n" f"Срок: {valid_period[:30]}" ) text_rect = fitz.Rect(x0 + 8, y0 + 6, x1 - 8, y1 - 6) page.insert_textbox(text_rect, text, fontsize=7.5, color=(0.05, 0.15, 0.45)) out_bytes = doc.tobytes() doc.close() return out_bytes except Exception as e: log.error(f"Failed to stamp PDF: {e}") return pdf_bytes def append_signature_certificate_page(pdf_bytes, sig_info): """ Создает и добавляет в конец PDF отдельную страницу «Свидетельство об электронной подписи» с полными реквизитами. """ try: doc = fitz.open(stream=pdf_bytes, filetype="pdf") cert_page = doc.new_page(width=595, height=842) # A4 # Заголовок cert_page.draw_rect(fitz.Rect(40, 40, 555, 90), color=(0.2, 0.4, 0.8), fill=(0.94, 0.96, 1.0), width=1) cert_page.insert_text(fitz.Point(60, 68), "СВИДЕТЕЛЬСТВО ОБ ЭЛЕКТРОННОЙ ЦИФРОВОЙ ПОДПИСИ", fontsize=11, color=(0.1, 0.2, 0.5)) cert_page.insert_text(fitz.Point(60, 82), "Подтверждение подлинности электронного документа (ФЗ № 63-ФЗ)", fontsize=8, color=(0.3, 0.3, 0.3)) # Тело сертификата details = ( f"Статус проверки: ПОДТВЕРЖДЕНА\n\n" f"Владелец сертификата:\n{sig_info.get('owner', 'Не указан')}\n\n" f"Организация / Ведомство:\n{sig_info.get('org', 'Не указана')}\n\n" f"Серийный номер сертификата:\n{sig_info.get('serial', 'Не указан')}\n\n" f"Кем выдан сертификат:\n{sig_info.get('issuer', 'Удостоверяющий центр')}\n\n" f"Срок действия:\nС {sig_info.get('valid_from', '-')} по {sig_info.get('valid_to', '-')}\n\n" f"Дата обработки в системе Домовой 2.0:\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) cert_page.insert_textbox(fitz.Rect(50, 110, 545, 450), details, fontsize=9.5, color=(0.15, 0.15, 0.15)) out_bytes = doc.tobytes() doc.close() return out_bytes except Exception as e: log.error(f"Failed to append certificate page: {e}") return pdf_bytes if __name__ == "__main__": print("Signature Verifier ready.")