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
75 changes: 75 additions & 0 deletions .github/workflows/test-latest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Постоянная тестовая сборка форка: тег test-latest всегда указывает
# на последний коммит, а SignService.exe по одной и той же ссылке
# заменяется. На zaynullinmi/SignService задание не выполняется.
name: Test build

on:
push:
branches:
- cursor/v1.3.1-ui-tweaks-cb6f
- cursor/cosign-discovery-gost-cb6f
- main
workflow_dispatch:

permissions:
contents: write

concurrency:
group: test-latest
cancel-in-progress: false

jobs:
publish-test-latest:
name: Тестовая сборка test-latest
if: github.repository == 'sozertsatel/SignService'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

# Те же флаги, что у задания «Publish self-contained single-file exe»
# в .github/workflows/build.yml.
- name: Publish self-contained single-file exe
run: >
dotnet publish src/SignService -c Release -r win-x64
--self-contained true
-p:PublishSingleFile=true
-p:IncludeNativeLibrariesForSelfExtract=true
-p:EnableCompressionInSingleFile=true
-o publish

- name: SHA-256
working-directory: publish
run: sha256sum SignService.exe | tee SignService.exe.sha256

- name: Обновить pre-release test-latest
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
version="$(sed -n 's/.*<Version>\([^<]*\)<\/Version>.*/\1/p' src/SignService/SignService.csproj | head -1)"
sha="$(git rev-parse HEAD)"
notes="$(printf '%s\n' \
"Тестовая сборка форка, не выпуск для пользователей. Автообновление SignService запрашивает только релизы https://github.com/zaynullinmi/SignService/releases/latest и этот pre-release не видит." \
"" \
"- Версия: ${version}" \
"- Коммит: ${sha}" \
"- Ветка: ${GITHUB_REF_NAME}" \
"" \
"https://github.com/sozertsatel/SignService/releases/download/test-latest/SignService.exe")"
if gh release view test-latest >/dev/null 2>&1; then
gh release delete test-latest --yes --cleanup-tag
fi
# Тег без релиза тоже сдвигаем, иначе create останется на старом коммите.
git push origin ":refs/tags/test-latest" || true
gh release create test-latest \
publish/SignService.exe \
publish/SignService.exe.sha256 \
--target "$sha" \
--title "Тестовая сборка (последняя)" \
--prerelease \
--latest=false \
--notes "$notes"
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# История версий SignService

## v1.3.3 — 12.10.2026

- **Резервная копия .sig.** В «Настройках…» — галочка «Всегда создавать
резервную копию .sig (.bak) перед подписанием», по умолчанию выключена.
Без неё копия по-прежнему создаётся только в рискованных случаях: замена
файла подписи без объединения, исключение подписантов или пропажа
документа, вложенного в прикреплённую подпись. С галочкой .bak пишется
перед каждой перезаписью существующего .sig, в том числе при обычном
дописывании.
- **«Подписать»** стоит под списком файлов у правого края, как «Очистить список».
- **«Очистить лог»** перенесена в нижнюю строку, слева, в один ряд с «Настройки…».
Стирает только лог операций; «Очистить список» по-прежнему очищает очередь файлов.
- Если файл подписи успели изменить, пока шло подписание (другая программа,
синхронизация папки), запись отменяется: на диске остаётся более новая версия.
- **«Применить ко всем»** в режиме «Создать новую» прямо в строке пишет,
какие уже найденные подписи не войдут в объединение.
- **Версия на главном окне.** Строка с автором убрана (автор остаётся
в «О программе»). На её месте — версия программы.
- «Обзор…» и «Инструменты» стоят над списком файлов в одной строке,
«Очистить список» — у правого края.
- Пункт «Объединить .sig…» убран из меню «Инструменты»: соподписание
выбирается в строке файла.
- **«Применить ко всем»** рядом с «Инструменты». Все файлы очереди,
включая добавленные позже, берут режим первого файла. Если добавить
к существующей подписи нельзя, строка остаётся на «Создать новую»
и пишет об этом. Состояние галочки запоминается.

## v1.3.0 — 09.10.2026

- **Проверка ГОСТ-подписей КриптоПро.** Подписи, у которых в signatureAlgorithm
Expand Down
14 changes: 14 additions & 0 deletions src/SignService/Services/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ public AppSettings(string settingsDirectory)
/// <summary>Объединять свою подпись с существующим .sig (соподписание).</summary>
public bool MergeWithExisting { get; set; } = true;

/// <summary>
/// Сохранять .bak перед любой перезаписью существующего .sig.
/// По умолчанию выключено: копия всё равно создаётся, если файл заменяется
/// без объединения, из него исключаются подписанты или пропадает документ,
/// вложенный в прикреплённую подпись.
/// </summary>
public bool CreateSignatureBackup { get; set; }

/// <summary>
/// Все файлы очереди используют режим первого файла
/// («Создать новую» или «Добавить к существующей»).
/// </summary>
public bool ApplyCoSignToAll { get; set; }

/// <summary>Добавлять штамп времени TSA (CAdES-T) в подпись.</summary>
public bool UseTimestamp { get; set; }

Expand Down
80 changes: 71 additions & 9 deletions src/SignService/Services/DocumentSigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ public sealed record SignOptions
/// <summary>Объединять ли с уже существующим файлом .sig (соподписание).</summary>
public bool MergeWithExisting { get; init; }

/// <summary>
/// Сохранять .bak перед любой перезаписью существующего .sig, в том числе
/// при обычном дописывании. Без этого копия создаётся только в рискованных
/// случаях: замена без объединения, исключение подписантов или потеря
/// документа, вложенного в прикреплённую подпись.
/// </summary>
public bool CreateBackup { get; init; }

/// <summary>
/// Вызывается после подготовки новой подписи и до её записи.
/// Нужен тестам, чтобы подменить файл подписи в этот промежуток.
/// </summary>
internal Action? BeforePublish { get; init; }

/// <summary>Пути к .sig других подписантов для объединения.</summary>
public IReadOnlyList<string> ExtraSignatures { get; init; } = Array.Empty<string>();

Expand Down Expand Up @@ -227,9 +241,15 @@ void AddInputPath(string? path)

var appended = inputPaths.Any(path => string.Equals(
Path.GetFullPath(path), Path.GetFullPath(signaturePath), StringComparison.OrdinalIgnoreCase));
var previousSignerCount = appended
? CmsMerger.CountSigners(await File.ReadAllBytesAsync(signaturePath, cancellationToken))
: 0;
// Снимок того, что вошло в объединение. Перед записью файл читается ещё раз:
// за время PIN-кода или пакетного подписания его могли обновить.
byte[]? previousBytes = null;
var previousSignerCount = 0;
if (appended)
{
previousBytes = await File.ReadAllBytesAsync(signaturePath, cancellationToken);
previousSignerCount = CmsMerger.CountSigners(previousBytes);
}

int signerCount;
IReadOnlyList<string> excluded;
Expand All @@ -256,13 +276,21 @@ void AddInputPath(string? path)
cancellationToken.ThrowIfCancellationRequested();
if (!(await File.ReadAllBytesAsync(targetPath, cancellationToken)).AsSpan().SequenceEqual(data))
throw new IOException("Документ изменился во время подписания. Подпись не сохранена; повторите операцию.");
if (poaPackage is not null) PowerOfAttorneyService.CopyPackage(poaPackage, targetPath);
// Резервная копия — только когда прежний файл теряет данные: он заменяется без
// объединения или из него исключаются подписанты. Дописанная подпись сохраняет
// всех прежних подписантов байт в байт, копия тогда не нужна.
options.BeforePublish?.Invoke();
// Рискованные случаи — всегда: файл заменяется без объединения, из него
// исключаются подписанты или пропадает документ внутри прикреплённой подписи.
// Галочка добавляет копию и при обычном дописывании. Если файл успели
// изменить после чтения — не пишем ничего, в том числе файлы МЧД.
string? backupPath = null;
if (File.Exists(signaturePath) && (!appended || excluded.Count > 0))
backupPath = SignatureBackup.Create(signaturePath);
if (File.Exists(signaturePath))
{
var latest = await File.ReadAllBytesAsync(signaturePath, cancellationToken);
ThrowIfSignatureChanged(previousBytes, latest);
var riskyOverwrite = !appended || excluded.Count > 0 || LosesEmbeddedDocument(latest, output);
if (riskyOverwrite || options.CreateBackup)
backupPath = SignatureBackup.Create(signaturePath);
}
if (poaPackage is not null) PowerOfAttorneyService.CopyPackage(poaPackage, targetPath);
await AtomicFile.WriteAsync(signaturePath, output, overwrite: true, cancellationToken);

// Режим «копия отдельно»: штампованная копия БЕЗ подписи, со всеми
Expand Down Expand Up @@ -294,6 +322,40 @@ void AddInputPath(string? path)
previousSignerCount, appended, backupPath);
}

/// <summary>
/// Отказ от записи, если файл подписи сменился после того, как его прочитали
/// для объединения. Иначе более новая чужая подпись была бы затёрта нашей копией.
/// </summary>
internal static void ThrowIfSignatureChanged(byte[]? snapshot, byte[] latest)
{
if (snapshot is not null && !snapshot.AsSpan().SequenceEqual(latest))
throw new IOException(
"Файл подписи изменился во время подписания. Подпись не сохранена; повторите операцию.");
}

/// <summary>
/// true, если в существующей прикреплённой подписи есть документ, а в новой
/// записи его не будет или он станет другим. Сами значения подписей при этом
/// могут сохраниться — пропадает копия документа внутри .sig.
/// </summary>
internal static bool LosesEmbeddedDocument(byte[] existing, byte[] output)
{
byte[]? embedded;
try { embedded = CmsMerger.ExtractContent(existing); }
catch (Exception) { return true; }
if (embedded is null)
return false;
try
{
var kept = CmsMerger.ExtractContent(output);
return kept is null || !kept.AsSpan().SequenceEqual(embedded);
}
catch (Exception)
{
return true;
}
}

/// <summary>
/// Формирует CMS/PKCS#7 подпись для массива байтов.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/SignService/SignService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<AssemblyTitle>SignService — подписание документов ЭЦП</AssemblyTitle>
<Product>SignService</Product>
<Version>1.3.0</Version>
<Version>1.3.3</Version>
<Authors>Зайнуллин Марат Илгамович</Authors>
<Company>Зайнуллин Марат Илгамович</Company>
<Copyright>© 2026 Зайнуллин Марат Илгамович, +7-963-694-2461, zaynullinmi@gmail.com</Copyright>
Expand Down
Loading
Loading