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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# История версий SignService

## v1.3.0 — 09.10.2026

- **Проверка ГОСТ-подписей КриптоПро.** Подписи, у которых в signatureAlgorithm
записаны параметры ГОСТ-ключа (так подписывают КриптоПро и CryptoAPI, в том
числе сама SignService на Windows), в 1.2.0 получали «не определено»
и при объединении помечались как непроверенные. Теперь они проверяются
полностью: значение подписи, привязка к документу, сертификат.
- Подпись, в подписанных атрибутах которой нет messageDigest, считается
не соответствующей документу.
- **Соподписание по содержимому документа.** При добавлении документа в его
папке в фоне ищутся .sig, .p7s и .p7m, относящиеся к нему по содержимому, —
даже если имя подписи другое («договор-.pdf .sig»). У каждого файла — выбор
«Добавить к „X.sig“ (8 подписантов)» или «Создать новую» и строка плана:
«будет добавлена подпись №9 к …». Переключатель «Добавить к существующей
подписи / Создать новую» задаёт выбор по умолчанию.
- Перед заменой существующего файла подписи без объединения и перед
исключением подписантов, чья подпись не соответствует документу, программа
спрашивает подтверждение и сохраняет рядом резервную копию
(`имя.ггггММдд-ЧЧммсс.bak`).
- После подписания видно число подписантов: «Подписан: 9 (8 + ваша) → X.sig»;
после смены сертификата подпись дописывается в получившийся файл.
- Подпись, перетащенная в окно из другой папки, прикладывается к своему
документу по содержимому и объединяется с вашей; сам файл не меняется.
Подписи, не подошедшие ни к одному документу, перечисляются в логе.
- «Объединить .sig» называет результат «документ.sig», если документ известен
(в том числе найден по содержимому), и не затирает существующий файл.
- Режим «подписывать копию со штампом» не затрагивает подписи оригинала.
- Соподписание по содержимому и исправление проверки ГОСТ основаны на работе
в форке sozertsatel/SignService.

## v1.2.0 — 07.10.2026

- **Переработан интерфейс**: лог операций всегда виден в главном окне
Expand Down
26 changes: 20 additions & 6 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,22 @@ is published on the [Releases](https://github.com/zaynullinmi/SignService/releas
### Co-signing and verification

- other people's signatures are **merged with yours into a single `.sig`**
with multiple signers: automatically with an existing `name.sig` next to the
document (can be turned off), via the "+.sig" button, or by dropping
a signature file onto the window;
with multiple signers: the app finds a document's signatures in its folder
**by content** — the signature file name may be anything
("договор-.pdf .sig", `.p7s`, `.p7m`); each file offers "Add to 'X.sig'
(8 signers)" or "Create new", and a plan line shows what will happen
("signature No. 9 will be added to …"); the "Add to existing signature /
Create new" switch sets the default. Signatures from other folders are
attached with the "+.sig" button or by dropping them — to their document
by content;
- before replacing a signature file without merging and before excluding
signers the app asks for confirmation and keeps a backup next to it; the
result shows in the file row: "Подписан: 9 (8 + ваша) → X.sig";
- re-signing with the same certificate **replaces** your previous signature
instead of duplicating it;
- every merged signature is **cryptographically verified**, including the
document digest, signature value and CAdES certificate binding. Damaged
document digest, signature value and CAdES certificate binding — CryptoPro
signatures with GOST parameters in signatureAlgorithm included. Damaged
signatures and signatures over another revision are excluded; signers that
cannot be verified (unknown algorithm, missing certificate) are kept and
explicitly reported;
Expand All @@ -66,6 +75,8 @@ The operations are available in the **Tools** menu (pick files) and in the
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);
when the document is known (by name or found by content) the result is
`document.sig`, an existing file is never overwritten;
- **Extract from .sig…** — pull out of a container: the embedded document
(byte-exact), a detached signature with all signers, and individual `.sig`
files per signer (signer names in the file names);
Expand Down Expand Up @@ -206,8 +217,10 @@ dotnet publish src/SignService -c Release -r win-x64 --self-contained true \
## Usage

1. Start the app — certificates with a private key appear in the drop-down.
2. Pick a certificate and the signature mode; optionally enable the PDF stamp
(a dialog asks for its parameters); the timestamp is set up in **Settings…**.
2. Pick a certificate, the signature mode and "Add to existing signature" or
"Create new"; each file shows which signature yours will be added to.
Optionally enable the PDF stamp (a dialog asks for its parameters); the
timestamp is set up in **Settings…**.
3. Drag files into the window (or use the browser) and press **Sign** —
a `name.sig` appears next to each file.
4. Operations on existing signatures live in the **Tools** menu and in the
Expand Down Expand Up @@ -236,6 +249,7 @@ src/SignService/
│ ├── Streebog.cs # GOST R 34.11-2012 for certHash (RFC 6986 vectors)
│ ├── CmsMerger.cs # ASN.1-level merge/split/build of signatures
│ ├── CmsExtractor.cs # file operations: extract, merge, split, container
│ ├── SignatureDiscovery.cs # finding a document's signatures by content
│ ├── SignatureVerifier.cs # per-signer verification (BouncyCastle, GOST without a CSP)
│ ├── CertificateValidator.cs # trust chain (PKIX) and OS stores
│ ├── RevocationChecker.cs # certificate revocation: CRL and OCSP
Expand Down
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,21 @@

### Соподписание и проверка

- подписи других лиц **объединяются с вашей в один `.sig`**: автоматически
с существующей `имя.sig` рядом (отключается), кнопкой «+.sig» у файла
или перетаскиванием файла подписи в окно;
- подписи других лиц **объединяются с вашей в один `.sig`**: программа сама
находит подписи документа в его папке **по содержимому** — имя файла подписи
может быть любым («договор-.pdf .sig», `.p7s`, `.p7m`); у каждого файла
выбирается «Добавить к „X.sig“ (8 подписантов)» или «Создать новую», а строка
плана показывает, что произойдёт («будет добавлена подпись №9 к …»);
переключатель «Добавить к существующей подписи / Создать новую» задаёт выбор
по умолчанию. Подписи из других папок прикладываются кнопкой «+.sig» или
перетаскиванием — к своему документу по содержимому;
- перед заменой файла подписи без объединения и перед исключением подписантов
программа спрашивает подтверждение и сохраняет резервную копию рядом;
итог виден в строке файла: «Подписан: 9 (8 + ваша) → X.sig»;
- повторная подпись тем же сертификатом **заменяет** свою старую, а не дублирует;
- каждая объединяемая подпись **проверяется криптографически**: хеш документа,
значение подписи и привязка сертификата CAdES. Повреждённые подписи и подписи
значение подписи и привязка сертификата CAdES — в том числе подписи КриптоПро
с параметрами ГОСТ в signatureAlgorithm. Повреждённые подписи и подписи
под другой версией исключаются с указанием подписанта; подписи, которые проверить
не удалось (неизвестный алгоритм, нет сертификата), сохраняются и явно отмечаются;
- приложенная подпись другого лица или смена сертификата возвращают уже подписанный
Expand All @@ -58,6 +67,8 @@
сразу. Отчёт можно скопировать или сохранить в TXT;
- **Объединить .sig…** — несколько файлов подписей склеиваются в один со всеми
подписантами (принимаются и прикреплённые контейнеры — документ сохраняется);
если документ известен (по имени или найден по содержимому), результат —
«документ.sig», существующий файл не затирается;
- **Извлечь из .sig…** — из криптоконтейнера достаются: вложенный документ
(байт-в-байт), откреплённая подпись со всеми подписантами и отдельные `.sig`
по каждому подписанту (в имени — ФИО из сертификата);
Expand Down Expand Up @@ -196,8 +207,10 @@ dotnet publish src/SignService -c Release -r win-x64 --self-contained true \
## Как пользоваться

1. Запустите программу — в списке появятся сертификаты с закрытым ключом.
2. Выберите сертификат и режим подписи; при необходимости включите штамп
на PDF (параметры спросит диалог), штамп времени — в «Настройках…».
2. Выберите сертификат, режим подписи и «Добавить к существующей подписи» или
«Создать новую»; у каждого файла видно, к какой подписи добавится ваша.
При необходимости включите штамп на PDF (параметры спросит диалог),
штамп времени — в «Настройках…».
3. Перетащите файлы в окно (или «Обзор…») и нажмите **«Подписать»** —
рядом с каждым файлом появится `имя.sig`.
4. Операции с готовыми подписями — в меню **«Инструменты»** и в контекстном
Expand Down Expand Up @@ -225,6 +238,7 @@ src/SignService/
│ ├── Streebog.cs # ГОСТ Р 34.11-2012 для certHash (векторы RFC 6986)
│ ├── CmsMerger.cs # объединение/разбор/сборка подписей на ASN.1
│ ├── CmsExtractor.cs # файловые операции: извлечение, объединение, разделение
│ ├── SignatureDiscovery.cs # поиск подписей документа по содержимому
│ ├── SignatureVerifier.cs # проверка подписантов (BouncyCastle, ГОСТ без CSP)
│ ├── CertificateValidator.cs # цепочка доверия (PKIX) и хранилища ОС
│ ├── RevocationChecker.cs # отзыв сертификатов: CRL и OCSP
Expand Down
40 changes: 29 additions & 11 deletions src/SignService/Services/CmsExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ public static MergeFilesResult MergeSignatureFiles(IReadOnlyList<string> paths)

var inputs = paths.Select(File.ReadAllBytes).ToList();

// Документ: из прикреплённого контейнера либо из файла рядом с .sig.
// Документ: из прикреплённого контейнера, из файла «имя.sig» → «имя»
// либо — если имя подписи нестандартное — по хешу содержимого в том же каталоге.
byte[]? document = null;
string documentNote = "";
string? knownDocumentName = null;
foreach (var input in inputs)
{
var content = CmsMerger.ExtractContent(input);
Expand All @@ -98,15 +100,25 @@ public static MergeFilesResult MergeSignatureFiles(IReadOnlyList<string> paths)
{
foreach (var path in paths)
{
if (!path.EndsWith(".sig", StringComparison.OrdinalIgnoreCase))
var documentPath = SignatureDiscovery.SiblingDocumentPath(path);
if (documentPath is null || !File.Exists(documentPath))
continue;
var documentPath = path[..^4];
if (File.Exists(documentPath))
{
document = File.ReadAllBytes(documentPath);
documentNote = $"проверено по документу «{Path.GetFileName(documentPath)}»";
break;
}
document = File.ReadAllBytes(documentPath);
knownDocumentName = Path.GetFileName(documentPath);
documentNote = $"проверено по документу «{knownDocumentName}»";
break;
}
}

var directory = Path.GetDirectoryName(paths[0]) ?? ".";
if (document is null)
{
var found = SignatureDiscovery.FindDocumentByContent(directory, paths);
if (found is not null)
{
document = File.ReadAllBytes(found);
knownDocumentName = Path.GetFileName(found);
documentNote = $"проверено по документу «{knownDocumentName}» (найден по хешу содержимого, не по имени файла подписи)";
}
}

Expand All @@ -128,15 +140,21 @@ public static MergeFilesResult MergeSignatureFiles(IReadOnlyList<string> paths)

var info = CmsMerger.Inspect(merged);

var directory = Path.GetDirectoryName(paths[0]) ?? ".";
var firstName = Path.GetFileName(paths[0]);
var baseName = firstName.EndsWith(".sig", StringComparison.OrdinalIgnoreCase)
|| firstName.EndsWith(".p7s", StringComparison.OrdinalIgnoreCase)
|| firstName.EndsWith(".p7m", StringComparison.OrdinalIgnoreCase)
? firstName[..^4]
: firstName;

var outputPath = UniquePath(directory, $"{baseName} (объединённая).sig", paths);
// Документ известен — результат называется «документ.sig», а не повторяет
// странное имя входного файла. Существующий файл с таким именем не затирается.
var desiredName = knownDocumentName is not null
? knownDocumentName + ".sig"
: $"{baseName} (объединённая).sig";
var outputPath = UniquePath(directory, desiredName, paths);
if (!string.Equals(Path.GetFileName(outputPath), desiredName, StringComparison.OrdinalIgnoreCase))
documentNote += $" Существующий файл «{desiredName}» не перезаписан, результат записан в «{Path.GetFileName(outputPath)}».";
AtomicFile.Write(outputPath, merged);

return new MergeFilesResult(
Expand Down
Loading
Loading