179 lines
8 KiB
Python
179 lines
8 KiB
Python
#!/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.")
|