Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches: ["main", "claude/**"]
tags: ["v*"]
pull_request:
workflow_dispatch:
inputs:
release_tag:
Expand All @@ -12,7 +13,7 @@ on:
default: ""

permissions:
contents: write # публикация GitHub Release на тег v*
contents: read

jobs:
tests:
Expand All @@ -30,7 +31,10 @@ jobs:

build-windows:
name: Windows x64 exe
needs: tests
runs-on: windows-latest
permissions:
contents: write # publication on a version tag or an explicitly requested release
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -60,5 +64,6 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.release_tag || github.ref_name }}
target_commitish: ${{ github.sha }}
files: publish/SignService.exe
body_path: CHANGELOG.md
23 changes: 20 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
# История версий SignService

## v1.1.0 — не выпущена
## v1.1.0 — 02.10.2026

- **Проверка ЭЦП без КриптоПро**: пункт «Инструменты → Проверить ЭЦП…»,
CMS/CAdES с ГОСТ-2001, ГОСТ-2012 256/512, RSA и ECDSA; отчёт по каждому
подписанту с экспортом в TXT и отменой проверки.
- Раздельные результаты для документа, значения подписи, привязки сертификата,
доверия, отзыва и TSA. Цепочки сертификатов проверяются без криптопровайдера;
вложенный корень автоматически не считается доверенным. Поддерживаются
подписанные CRL/OCSP и сетевые запросы по выбору; отсутствие подтверждений
отображается явно. TSA проверяется вместе с привязкой к подписи и запросу.
- Исправлены проверки объединения и МЧД: совпадение хеша больше не пропускает
повреждённое значение подписи; исключённый подписант не оставляет старый
вложенный документ, выбранный режим откреплённой подписи сохраняется.
- МЧД проверяется по текущим XML/SIG и копируется из проверенного снимка;
конфликт имён файлов и противоречивые ИНН/СНИЛС останавливают операцию.
Файлы подписи сохраняются через временный файл и атомарную замену.
- Ограничена вложенность ASN.1 и исправлен контроль переполнения длины.
Тесты запускаются для pull request; сборка Windows зависит от их успеха.

- **Штамп на PDF безопасен при соподписании**: по умолчанию подписывается
ОРИГИНАЛ документа, а копия «имя (со штампом).pdf» создаётся отдельно
без подписи — после подписания и со всеми подписантами итоговой подписи
(по блоку данных на каждого). Прежний режим «подписывать копию со
штампом» остался отдельной галочкой с предупреждением.
- **Доверенность МЧД** (кнопка «Добавить доверенность…», как в Контуре):
загрузка XML (EMCHD_1) и подписи руководителя, проверка подписи по хешу
(для ГОСТ — Стрибогом), срока действия и соответствия представителя
загрузка XML (EMCHD_1) и подписи руководителя, криптографическая проверка,
проверка срока действия и соответствия представителя
сертификату по ИНН/СНИЛС; при подписании файлы МЧД копируются рядом
с документом, номер МЧД добавляется в штамп на PDF.

Expand Down
48 changes: 42 additions & 6 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,19 @@ is published on the [Releases](https://github.com/zaynullinmi/SignService/releas
a signature file onto the window;
- re-signing with the same certificate **replaces** your previous signature
instead of duplicating it;
- every merged signature is **verified against the document hash**
(messageDigest; Streebog for GOST): signatures made over a different file
or an older revision are excluded automatically, reporting the signer name;
- every merged signature is **cryptographically verified**, including the
document digest, signature value and CAdES certificate binding. Damaged
signatures and signatures over another revision are excluded; unverifiable
signers are explicitly reported;
- input signatures are accepted in DER/BER (including indefinite lengths and
trailing bytes) and base64/PEM.

### Tools (no signature created)

- **Verify signature…** — per-signer CMS/CAdES verification for GOST-2001,
GOST-2012 256/512, RSA and ECDSA without CryptoPro. Select the original for
a detached signature; attached signatures use the embedded document.
Copy or export the detailed report to TXT;
- **Merge .sig…** — combine several signature files into one containing all
signers (attached containers are accepted; the embedded document is kept);
- **Extract from .sig…** — pull out of a container: the embedded document
Expand All @@ -65,8 +70,8 @@ is published on the [Releases](https://github.com/zaynullinmi/SignService/releas
- the "Add power of attorney…" button works like Kontur: pick the MChD XML
(EMCHD_1 format) and the head's signature (.sig; a neighbouring
`name.xml.sig` is picked up automatically);
- the app verifies: the head's signature matches the MChD file (by hash,
Streebog for GOST), the validity period has not expired, and the
- the app verifies the head's signature using the certificate public key,
checks the validity period and matches the
representative in the MChD matches the selected certificate by INN/SNILS;
- when signing, the MChD files (XML + .sig) are copied next to the signed
document (the MChD is NOT embedded into the CMS signature — Kontur does
Expand Down Expand Up @@ -108,11 +113,37 @@ is published on the [Releases](https://github.com/zaynullinmi/SignService/releas
- **auto-update**: new releases are checked on GitHub Releases at startup
(can be disabled) and installed in one click from the About window (Windows).

### Trust, revocation and timestamps

The report separates document integrity, signature value, certificate binding,
certificate trust, revocation and TSA results. An embedded root never creates
trust: anchors come from OS root stores and explicitly selected CA certificates.
BouncyCastle verifies certificate-chain, CRL and OCSP signatures without a CSP.

Verification is offline by default. It uses embedded CRLs and selected `.crl`/
`.ocsp` files; HTTP(S) CRL/OCSP requests require the network checkbox. Revocation
is checked for every chain certificate except the trust anchor. Missing, stale,
unverifiable or partial evidence (including delta/indirect CRLs) gives an unknown
status. Missing intermediate certificates must be embedded in the CMS or in OS
intermediate stores.

The signer's `signing-time` does not prove when a signature was created.
Historical certificate validity is assessed only using a TSA timestamp whose
imprint, signature, trust and required revocation checks passed. TSA responses
also must match the request and nonce.

MChD verification re-reads current files, verifies and copies the same XML/SIG
snapshot. Conflicting destination files stop signing before the signature is
saved. Cryptographic verification does not establish the head's authority,
the scope of delegated powers or MChD registry revocation; missing assurances
are reported as warnings.

## Requirements

- Windows 10/11 — the self-contained exe from the Releases page;
- for GOST signatures — **CryptoPro CSP** installed with a personal
- to **create** GOST signatures — **CryptoPro CSP** installed with a personal
certificate;
- verifying and merging GOST signatures requires **no CryptoPro** on Windows/Linux/macOS;
- building from source: .NET 8 SDK (Windows/Linux/macOS; outside Windows
signing is limited to platform algorithms — RSA/ECDSA; GOST requires
the CSP on Windows).
Expand All @@ -125,6 +156,11 @@ dotnet run --project src/SignService
dotnet run --project tests/SignService.Tests -c Release # tests
```

On a personal workstation append `-- --skip-user-store` to the test command to
skip scenarios that change user certificate stores or application data. CI runs
the full suite in an isolated runner. Managed GOST, CRL/OCSP and TSA verification
tests run without CryptoPro or OpenSSL.

Publishing the self-contained exe:

```bash
Expand Down
48 changes: 42 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@
с существующей `имя.sig` рядом (отключается), кнопкой «+.sig» у файла
или перетаскиванием файла подписи в окно;
- повторная подпись тем же сертификатом **заменяет** свою старую, а не дублирует;
- каждая объединяемая подпись **проверяется по хешу документа** (messageDigest;
для ГОСТ — Стрибогом): подписи под другим файлом или прежней версией
исключаются автоматически с указанием подписанта;
- каждая объединяемая подпись **проверяется криптографически**: хеш документа,
значение подписи и привязка сертификата CAdES. Повреждённые подписи и подписи
под другой версией исключаются с указанием подписанта; непроверенные явно отмечаются;
- входные подписи принимаются в DER/BER (включая неопределённые длины
и «хвосты» файлов) и base64/PEM.

### Инструменты (без создания подписи)

- **Проверить ЭЦП…** — проверка каждого подписанта CMS/CAdES: ГОСТ-2001,
ГОСТ-2012 256/512, RSA и ECDSA без КриптоПро. Для откреплённой подписи
выберите исходный документ; для прикреплённой он берётся из контейнера.
Отчёт можно скопировать или сохранить в TXT;
- **Объединить .sig…** — несколько файлов подписей склеиваются в один со всеми
подписантами (принимаются и прикреплённые контейнеры — документ сохраняется);
- **Извлечь из .sig…** — из криптоконтейнера достаются: вложенный документ
Expand All @@ -59,8 +63,8 @@
- кнопка «Добавить доверенность…» — как в Контуре: выбираются файл МЧД
(XML, формат EMCHD_1) и подпись руководителя (.sig; `имя.xml.sig` рядом
подхватывается автоматически);
- программа проверяет: подпись руководителя соответствует файлу МЧД
(по хешу, для ГОСТ — Стрибогом), срок действия не истёк, представитель
- программа проверяет подпись руководителя открытым ключом сертификата,
срок действия и совпадение представителя
в МЧД совпадает с владельцем выбранного сертификата по ИНН/СНИЛС;
- при подписании файлы МЧД (XML + .sig) копируются рядом с подписанным
документом (в CMS-подпись МЧД не вкладывается — так же поступает Контур),
Expand Down Expand Up @@ -104,10 +108,37 @@
- **автообновление**: проверка новых версий на GitHub Releases при запуске
(отключается) и обновление в один клик из окна «О программе» (Windows).

### Доверие, отзыв и время подписи

Отчёт разделяет соответствие документу, значение подписи, привязку сертификата,
цепочку доверия, статус отзыва и метку TSA. Корректная подпись сама по себе
не подтверждает доверие к сертификату. Вложенный корень не становится доверенным:
используются корни из хранилища ОС и явно выбранные в окне проверки сертификаты УЦ.
Подписи сертификатов цепочки, CRL и OCSP проверяются управляемой библиотекой BouncyCastle.

По умолчанию проверка работает без сети. Для отзыва используются вложенные CRL
и выбранные файлы `.crl`/`.ocsp`. Сетевые запросы к HTTP(S) службам CRL/OCSP УЦ
включаются отдельной галочкой. Просроченные, неподписанные, неполные (в том числе
delta/indirect CRL) или недоступные доказательства дают неопределённый статус,
а не «сертификат действителен»; данные об отзыве проверяются для всех сертификатов
цепочки, кроме доверенного корня. Недостающие промежуточные сертификаты
должны присутствовать в CMS или хранилище ОС.

`signing-time` — время, заявленное самим подписантом. Исторический срок сертификата
оценивается по TSA только после проверки хеша, подписи, доверия и требуемого статуса
отзыва TSA. При получении метки проверяются также nonce и соответствие запросу.

МЧД перечитывается перед подписанием, проверяется и копируется как один снимок XML + SIG.
Конфликт с другим файлом МЧД останавливает операцию до сохранения подписи.
Полномочия руководителя, содержание полномочий и отзыв самой МЧД в реестре
не устанавливаются одной криптографической проверкой; при отсутствии подтверждений
программа показывает предупреждение.

## Требования

- Windows 10/11 — готовый exe со страницы Releases (самодостаточный);
- для ГОСТ-подписей — установленный **КриптоПро CSP** и личный сертификат;
- для **создания** ГОСТ-подписей — установленный **КриптоПро CSP** и личный сертификат;
- проверка и объединение ГОСТ-подписей работают **без КриптоПро** на Windows/Linux/macOS;
- сборка из исходников: .NET 8 SDK (Windows/Linux/macOS; вне Windows
подписание доступно алгоритмами платформы — RSA/ECDSA, ГОСТ — только
через CSP на Windows).
Expand All @@ -120,6 +151,11 @@ dotnet run --project src/SignService
dotnet run --project tests/SignService.Tests -c Release # тесты
```

На рабочем компьютере используйте `-- --skip-user-store` в конце команды тестов,
чтобы пропустить сценарии, изменяющие личное хранилище сертификатов и данные приложения.
Полный набор запускается в изолированном CI. Управляемые тесты проверки ГОСТ, CRL/OCSP
и TSA выполняются без КриптоПро и OpenSSL.

Публикация самодостаточного exe:

```bash
Expand Down
3 changes: 3 additions & 0 deletions src/SignService/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("SignService.Tests")]
13 changes: 13 additions & 0 deletions src/SignService/Services/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Linq;

namespace SignService.Services;

Expand Down Expand Up @@ -66,6 +67,18 @@ public class AppSettings
/// <summary>Путь к подписи руководителя (.sig) для МЧД.</summary>
public string? PoaSigPath { get; set; }

/// <summary>Дополнительные явно доверенные корни; хранилище ОС не изменяется.</summary>
public List<string> VerificationTrustedRootPaths { get; set; } = new();

/// <summary>Разрешение запросов CRL/OCSP при проверке ЭЦП.</summary>
public bool VerificationAllowNetwork { get; set; }

public VerificationOptions CreateVerificationOptions(bool allowNetwork = false) => new()
{
TrustedRoots = VerificationTrustedRootPaths.Select(File.ReadAllBytes).ToArray(),
AllowNetwork = allowNetwork,
};

public static AppSettings Load()
{
try
Expand Down
35 changes: 35 additions & 0 deletions src/SignService/Services/AtomicFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace SignService.Services;

internal static class AtomicFile
{
public static async Task WriteAsync(string path, byte[] bytes, bool overwrite = true,
CancellationToken cancellationToken = default)
{
var fullPath = Path.GetFullPath(path);
var temporary = Path.Combine(Path.GetDirectoryName(fullPath)!, ".signservice-" + Guid.NewGuid().ToString("N") + ".tmp");
try
{
await using (var stream = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None,
8192, FileOptions.Asynchronous | FileOptions.WriteThrough))
{
await stream.WriteAsync(bytes, cancellationToken);
await stream.FlushAsync(cancellationToken);
stream.Flush(flushToDisk: true);
}
cancellationToken.ThrowIfCancellationRequested();
File.Move(temporary, fullPath, overwrite);
}
finally
{
if (File.Exists(temporary)) File.Delete(temporary);
}
}

public static void Write(string path, byte[] bytes, bool overwrite = false) =>
WriteAsync(path, bytes, overwrite).GetAwaiter().GetResult();
}
Loading
Loading