SignService: приложение подписания документов ЭЦП (v1.0 + МЧД, штампы) - #1
Merged
Merged
Conversation
Отдельная настольная программа на C#/.NET 8 + Avalonia 11 по мотивам сервиса подписания документов из ReportGGE: - сертификаты с закрытым ключом из хранилища «Личное» пользователя (для ГОСТ — через КриптоПро CSP на Windows); - откреплённая/прикреплённая подпись CMS/PKCS#7 (.sig) с атрибутом времени подписания, плюс проверка откреплённой подписи; - drag & drop файлов и папок в окно, добавление через диалог «Обзор…»; - пакетное подписание с постатусной индикацией по каждому файлу; - приём путей к файлам аргументами командной строки («Открыть с помощью»). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
- автор zaynullinmi с контактами в csproj, README и статусной строке окна; - workflow: публикация самодостаточного однофайлового SignService.exe (win-x64) артефактом на каждый пуш, а на тег v* — в GitHub Release. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Подписание приведено к схеме ReportGGE (Signing.cs): - на Windows подпись создаётся нативным CryptSignMessage (crypt32) — .NET SignedCms не умеет ГОСТ; CryptoAPI отдаёт операцию криптопровайдеру сертификата (КриптоПро CSP для ГОСТ); - OID хеша выбирается по типу ключа: ГОСТ Р 34.10-2012 (256/512) → 34.11-2012, ГОСТ Р 34.10-2001 → 34.11-94, иначе SHA-256; - в подпись вкладывается вся цепочка сертификатов (лист + УЦ + корень) для офлайн-проверки, DER-блобы цепочки кешируются по отпечатку; - выбранный сертификат и режим подписи запоминаются в %AppData%/SignService/settings.json (аналог SignCertThumbprint); - вне Windows остаётся SignedCms (RSA/ECDSA), для ГОСТ — понятная ошибка. Автор указан полностью: Зайнуллин Марат Илгамович, +7-963-694-2461, [email protected] (окно, README, метаданные сборки). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
В подписанные атрибуты каждой подписи теперь включаются: - время подписания (PKCS#9 signing-time); - signing-certificate-v2 (RFC 5035) — привязка подписи к сертификату подписанта, защита от подмены сертификата. Реализация: - CadesAttributes: DER-кодирование атрибутов через System.Formats.Asn1; - certHash для ГОСТ-2012 сертификатов считается Стрибогом (ГОСТ Р 34.11-2012, 256/512) — управляемая реализация Streebog.cs, проверенная на векторах RFC 6986 и случайных данных против эталонной реализации gostcrypto; для остальных ключей — SHA-256 (умолчание ESSCertIDv2); - NativeSign: поддержка подписанных атрибутов в CryptSignMessage (CRYPT_ATTRIBUTE; content-type и message-digest CryptoAPI добавляет автоматически); - managed-путь (SignedCms) добавляет те же атрибуты. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
- CmsMerger: слияние CMS/PKCS#7 подписей на уровне ASN.1 — объединяются SignerInfos, сертификаты, CRL и алгоритмы; работает и для ГОСТ-подписей на любой платформе (криптопровайдер не нужен). Повторная подпись того же подписанта (по SignerIdentifier) заменяет старую, а не дублируется. Вход принимается в DER/BER и base64/PEM; - при подписании своя подпись по умолчанию объединяется с уже существующим файлом .sig рядом с документом (чекбокс «Объединять с существующей .sig», настройка запоминается); - кнопка «+.sig» у файла — приложить подписи других лиц для объединения; перетаскивание/добавление файла «документ.pdf.sig» прикладывает его к «документ.pdf» из списка; - статус показывает число подписантов в итоговом файле. Тесты: слияние 2 и 3 подписей с криптопроверкой всех подписантов, замена своей подписи без дублирования, base64-вход, файловый сценарий (существующая .sig + приложенная + своя → 3 подписанта), режим замены. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Реальные .sig (КриптоПро, КриптоАРМ, порталы) часто закодированы в BER с неопределённой длиной (30 80 … 00 00) или длинной формой длины; при их объединении ASN.1-писатель .NET падал с ошибкой «single encoded value with no trailing data». - BerDer.ToDefinite: перекодирование BER → definite-length с сохранением структуры и порядка элементов; данные после первого TLV (хвост файла) отбрасываются; - CmsMerger.Normalize прогоняет каждый вход через эту нормализацию (после распознавания base64/PEM). Тесты: подпись, переведённая в indefinite-length BER с длинными формами длины, объединяется и проверяется; хвостовые байты после подписи не мешают; канонический DER нормализация возвращает без изменений. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Подпись, сделанная под другим файлом или прежней версией документа (как «№ 3» в логе портала), делала весь объединённый контейнер невалидным — портал отклонял его целиком («Не удалось улучшить подпись»). Теперь при объединении каждая подпись сверяется с документом по messageDigest из подписанных атрибутов: хеш документа считается по OID алгоритма подписанта (ГОСТ Р 34.11-2012 256/512 — своей реализацией Стрибога, SHA-1/256/384/512 — штатно). Несоответствующие подписи исключаются из результата, их подписанты (CN из вложенного сертификата) перечисляются в статусе файла; подписи, чей алгоритм проверить нечем, сохраняются с пометкой «не удалось проверить». Если документу не соответствует ни одна объединяемая подпись — понятная ошибка. Тесты: контейнер «валидная + протухшая» + приложенная + своя → 3 валидных подписанта, исключённый назван по имени, итог проходит криптопроверку; слияние из одних несоответствующих подписей падает с ясным сообщением; ГОСТ-OID ведёт на Стрибог-хеш документа. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Кнопка «Извлечь из .sig…» (поддерживаются .sig/.p7s/.p7m, base64/PEM, BER): - из прикреплённой подписи извлекается вложенный документ (байт-в-байт, включая BER-чанки OCTET STRING) и создаётся откреплённая подпись к нему со всеми подписантами; - контейнер с несколькими подписантами разбирается на отдельные откреплённые .sig по каждому подписанту, имя файла — CN из сертификата; - существующие файлы не перезаписываются (нумерованные копии), сам контейнер не изменяется. Реализация в CmsMerger (Inspect/ExtractContent/ConvertToDetached/ SplitBySigner) на ASN.1 — работает и для ГОСТ-подписей на любой платформе; файловые операции — CmsExtractor. Тесты: документ восстанавливается байт-в-байт, откреплённая версия проходит криптопроверку обоих подписантов, разбор даёт валидные одиночные подписи с ФИО в именах, повторное извлечение не перезаписывает, для откреплённой одиночной подписи корректно сообщается «нечего извлекать». Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Тип подписи (галочка «Штамп времени в подписи (CAdES-T)»): - TimestampClient — клиент службы штампов времени RFC 3161: значение подписи хешируется (для ГОСТ — Стрибогом, иначе SHA-256), полученный TimeStampToken вкладывается неподписанным атрибутом (1.2.840.113549.1.9.16.2.14) через новый CmsMerger.AddUnsignedAttribute на ASN.1 — подпись остаётся действительной, штамп переживает соподписание; адрес TSA настраивается и запоминается; - без галочки — прежняя усиленная подпись CAdES-BES. Визуальный штамп на PDF (галочка «Штамп на документ (PDF)»): - PdfStamper (PDFsharp): плашка «Документ подписан электронной подписью» с номером сертификата, владельцем, сроком действия, по желанию — датой подписания и логотипом организации (PNG/JPEG; PNG перекодируется через SkiaSharp, т.к. PDFsharp Core его не читает); - кириллица — встроенный шрифт DejaVu Sans (свободная лицензия); - штампованная копия «имя (со штампом).pdf» создаётся ДО подписания и подписывается именно она; исходный PDF не меняется; для не-PDF опция игнорируется, для битого PDF — понятная ошибка. Тесты: штамп времени через локальный RFC 3161-ответчик (openssl ts) — атрибут присутствует, токен декодируется, messageImprint совпадает с хешем значения подписи, подпись валидна, совместим с соподписанием; штамп на PDF — копия создаётся, подпись проверяется над штампованными байтами, варианты с датой/без и с лого/без, рендер проверен визуально. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Кнопка «Объединить .sig…»: несколько файлов подписей склеиваются в один со всеми подписантами без создания своей подписи (CmsExtractor.MergeSignatureFiles): - принимаются откреплённые .sig и прикреплённые криптоконтейнеры (.sig/.p7s/.p7m, DER/BER/base64); если среди входов есть контейнер с документом, результат тоже прикреплённый — документ сохраняется; - соответствие подписей проверяется по документу из прикреплённого контейнера либо по файлу рядом («документ.pdf.sig» → «документ.pdf»); несоответствующие исключаются с указанием подписанта; без документа — механическое объединение с пометкой в статусе; - результат «имя (объединённая).sig» рядом с первым входом, входные файлы не перезаписываются. Интеграционные тесты перенесены в репозиторий (tests/SignService.Tests — Стрибог по векторам RFC 6986, CAdES-BES, соподписание, BER-нормализация, извлечение, CAdES-T через локальный RFC 3161-ответчик на openssl, штамп на PDF, объединение без подписания) и запускаются в CI отдельным job на ubuntu. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Сохранение сертификата на компьютер (кнопки 💾/🗑 у списка): - CertificateVault: экспорт сертификата с закрытым ключом в PFX, защищённый паролем (%AppData%/SignService/certificates), чтобы подписывать без подключённого ключа ЭЦП; диалог сохранения явно предупреждает о снижении безопасности и требует двойной ввод пароля; - сохранённые сертификаты видны в списке («сохранён на ПК 🔒», открытая часть без пароля), при подписании пароль спрашивается один раз на сеанс; - удаление затирает PFX-файл перед удалением; неэкспортируемый ключ даёт понятную ошибку. Операции без подписания (сгруппированы в меню «Инструменты»): - «Собрать контейнер…» — CmsMerger.AttachContent: документ упаковывается в прикреплённый .sig с имеющимися подписями (из «имя.sig» рядом или выбранными вручную), с проверкой подписей по документу; - «Штамп на PDF…» — копия «имя (со штампом).pdf» без подписания. Окно лога (кнопка «Лог»): все смены статуса с отметкой времени плюс построчные результаты пакетного подписания; очистка и автопрокрутка. Диалоги PasswordDialog (с режимом предупреждения) и ConfirmDialog. Тесты: PFX-цикл save→load→sign→verify, неверный пароль, удаление с затиранием; сборка контейнера с проверкой подписей и исключением протухшей, обратимость контейнер↔извлечение, ручной выбор подписей, ошибка без подписей. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Меню кнопки 💾 у списка сертификатов теперь предлагает два режима: - «Сохранить в программе (PFX с паролем)» — как раньше, виден только SignService; - «Установить в хранилище Windows (для всех программ)» — сохранённый в программе сертификат импортируется в системное хранилище «Текущий пользователь → Личное» (PersistKeySet, ключ сохраняется), после чего им могут подписывать любые программы: КриптоАРМ, браузер и т.д. Диалог предупреждает, что ключ станет доступен всем программам учётной записи без пароля этой программы. Меню 🗑 дополнено пунктом «Удалить из хранилища Windows» — программа разрешает удалять только сертификаты, которые сама установила (список отпечатков в настройках); чужие записи хранилища не трогаются. Тесты: установка в системное хранилище → сертификат виден с закрытым ключом и подписывает «как из другой программы», подпись проверяется; удаление из хранилища; отказ удалять чужой сертификат. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Пункт «Установить в хранилище Windows» был доступен только для сертификатов с пометкой «сохранён на ПК» — для сертификата, видимого из системного хранилища (ключ на токене), он оставался неактивным. Теперь: - пункт доступен и для сертификата из хранилища с закрытым ключом: одним действием ключ экспортируется в PFX программы (пароль, двойной ввод, развёрнутое предупреждение) и его копия устанавливается в хранилище — подписание в любых программах без токена; - InstallToStore заменяет существующую запись хранилища с тем же отпечатком (иначе Windows сохранила бы старую привязку к токену и подписание по-прежнему требовало бы его); - команды кнопок 💾/🗑 и «Инструментов» обновляют доступность при занятости (IsBusy). Тест: установка при уже существующей записи хранилища — запись заменяется, остаётся одна, с ключом из PFX. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
- UpdateService: проверка последнего релиза GitHub (releases/latest), сравнение версий, скачивание SignService.exe и самозамена на Windows (cmd-скрипт ждёт выхода, подменяет exe и перезапускает); тихая проверка при запуске (отключается в настройках); - окно «О программе»: версия, контакты автора, ссылка на проект, история версий из встроенного CHANGELOG.md, проверка и установка обновлений; кнопка в статусной строке; - CHANGELOG.md (встраивается в сборку) — история версий, v1.0.0; - README.md переработан (все функции, ссылка на Releases) + перевод README.en.md с взаимными ссылками; - workflow: permissions contents:write и body_path CHANGELOG.md для публикации релиза на тег v*; - тесты: разбор ответа GitHub Releases (новее/та же/более старая/битый тег), формат текущей версии, наличие встроенного CHANGELOG. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Пуш тегов из среды разработки недоступен, поэтому релиз можно выпустить через workflow_dispatch: параметр release_tag (например v1.0.0) — softprops/action-gh-release создаёт тег на текущем коммите и публикует релиз с SignService.exe и описанием из CHANGELOG.md. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Разбор реальных подписей Контура показал: специальных атрибутов МЧД в CMS-подписи нет — базовая подпись Контура совпадает по формату с нашей (CAdES-BES), а доверенность передаётся файлами рядом (XML + .sig руководителя). Реализовано так же: - PowerOfAttorneyService: разбор EMCHD_1 XML (номер, срок, доверитель, представитель), проверка подписи руководителя по хешу (для ГОСТ — Стрибогом), срока действия и соответствия представителя сертификату по ИНН (1.2.643.3.131.1.1) и СНИЛС (1.2.643.100.3) из subject; - CmsMerger.CheckAgainstDocument — публичная сверка подписи с файлом; - кнопка «Добавить доверенность…»: XML + .sig (имя.xml.sig рядом подхватывается сам), итог проверки в статусе; доверенность запоминается и перепроверяется перед каждым подписанием; - при подписании файлы МЧД копируются в каталог подписанного документа; - в PDF-штамп добавляется строка «Действует на основании МЧД № …». Проверено на реальных файлах (МЧД ООО «ВЕКТОР» + подпись руководителя ГОСТ + сертификат Сертум-Про): разбор, проверка подписи и совпадение представителя. Тесты: разбор EMCHD, совпадение/несовпадение представителя, подпись под другим файлом, истёкший срок, копирование файлов при подписании. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
При прежней схеме (штамп до подписи, подписывается штампованная копия) соподписание ломалось: штамп следующего подписанта менял файл и делал прежние подписи недействительными. Теперь по умолчанию подписывается ОРИГИНАЛ документа, а копия «имя (со штампом).pdf» создаётся отдельно БЕЗ подписи — после подписания, со всеми подписантами итоговой .sig (блок данных на каждого; сертификаты подписантов достаются из контейнера новым CmsMerger.GetSignerCertificates). Копия пересоздаётся при каждом соподписании. Прежний режим — галочка «подписывать копию со штампом» с предупреждением. Тесты: режим sign-copy сохранён; в режиме отдельной копии оригинал не меняется, обе подписи двух подписантов валидны против оригинала, копия пересоздаётся с блоками обоих подписантов; GetSignerCertificates возвращает сертификаты обоих. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Вся разработка SignService: Avalonia-приложение подписания документов ЭЦП (CAdES-BES/T, ГОСТ через КриптоПро, Стрибог), соподписание и проверка подписей, инструменты (объединение/извлечение/сборка контейнера), визуальный штамп на PDF (по умолчанию — отдельной копией без подписи, безопасно при соподписании), доверенность МЧД (EMCHD_1), сертификат без токена (PFX/хранилище Windows), автообновление, окно «О программе», README RU/EN, CHANGELOG, интеграционные тесты и CI (тесты + сборка exe + релизы).
После мержа README будет виден на главной странице репозитория.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BtHSRXSuM8qTBTUBe29624
Generated by Claude Code