mutt/signature_verifier.py

179 lines
8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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.")