💡 Повний робочий приклад доступний на GitHub:
digital-signing-certificate-validity-dotnet

Проблема відповідності, яку ніхто не бачить, доки не з’явиться аудитор

Сервіс підпису працює три роки без помилок. Документи надсилаються, одержувачі їх приймають, у журналах немає жодних ознак проблеми. Потім валідатор контрагента позначає партію як недійсну, і розслідування виявляє дві причини: підписи були створені з використанням SHA‑1, а протягом останніх чотирьох місяців сертифікат був прострочений.

Обидві помилки залишалися непоміченими у момент підписання. Це саме те, що змінює GroupDocs.Signature 26.9.

Примусове дотримання терміну дії сертифіката стає новою поведінкою за замовчуванням для .NET цифрового підпису: сертифікат, який знаходиться поза межами свого терміну дії, відхиляється, а не використовується. Він постачається разом з двома супутниками — SHA‑256 як алгоритм хешування PDF за замовчуванням і LogLevel, який нарешті фільтрує — і разом вони переміщують три класи помилок від одержувача назад до відправника, де їх ще можна виправити.

Чому «тихий» успіх — це дорогий результат

Підписання незвичне тим, що сторона, яка робить помилку, не є тією, хто її виявляє. Некоректний рахунок‑фактура не проходить у вашій системі; недійсний підпис не проходить у когось іншого, тижнями пізніше, без можливості діагностувати проблему.

Ця асиметрія пояснює, чому «API повернув успіх» не є корисною гарантією. Старі налаштування оптимізувалися під те, щоб не переривати виклик, а вартість перекладалася на одержувача і, зрештою, на того, хто змушений був повторно підписати і повторно надіслати кілька сотень документів.

Зміна 1: Прострочені сертифікати відхиляються

Головна зміна. Sign тепер викидає GroupDocsSignatureException, коли термін дії сертифіката закінчився або ще не розпочався, і нічого не записується на диск.

try
{
    signature.Sign(outputPath, options);
    return true;
}
catch (GroupDocsSignatureException ex)
{
    Console.WriteLine($"   Rejected: {ex.Message}");
    return false;
}

Повідомлення містить назву сертифіката та властивість, яка дозволила б його використання, тому оператор, читаючи рядок журналу, може одразу вжити заходів, не відкриваючи документацію. Для конвеєра, який оновлюється до 26.9 і починає падати, це майже завжди причина — і правильна реакція — оновити сертифікат, а не придушувати помилку.

Коли дійсно потрібна стара поведінка, існує одна властивість:

var options = new DigitalSignOptions(certificate)
{
    Password = certificatePassword,
    AllowExpired = true
};

Документ підписується, а попередження надходить у логгер. Валідатори все одно відхиляють результат, бо AllowExpired керує тим, що бібліотека дозволяє, а не тим, що вартує сертифікат. Супутня прапорець AllowNotYetValid охоплює інший кінець вікна і навмисно незалежний: дозволити прострочений сертифікат не означає тихо дозволити сертифікат, датований у майбутньому.

Зміна 2: SHA‑256 за замовчуванням

Цифрові підписи PDF тепер створюються з використанням SHA‑256 у форматі adbe.pkcs7.detached, який очікують сучасні валідатори. Попередні версії використовували SHA‑1.

var options = new DigitalSignOptions(certificate)
{
    Password = certificatePassword,
    HashAlgorithm = HashAlgorithm.Sha256,
    Reason = "Approved",
    Location = "Head office"
};

Явне встановлення властивості необхідне лише для переходу далі — Sha384 або Sha512, коли політика їх вимагає, — або для залишення Sha1 у випадку валідатора, який не підтримує інші алгоритми. Часова мітка, додана до підпису, використовує той самий хеш.

Перевірка змінилася в тому ж випуску і в тому ж напрямку: DigitalVerifyOptions без критеріїв раніше майже нічого не робив, а тепер виконує повну криптографічну перевірку, тому документ, змінений після підписання, буде позначений як недійсний.

Зміна 3: LogLevel дійсно фільтрує

SignatureSettings давно приймає логгер. До 26.9 рівень ігнорувався, тому кожне повідомлення надходило незалежно, і більшість сервісів вимикали логування, а не тонули у трасах.

У прикладі різницю можна виміряти, підписавши один і той самий документ тричі за допомогою підраховуючого логгера:

var levels = new Dictionary<string, LogLevel>
{
    ["None"] = LogLevel.None,
    ["Warning | Error"] = LogLevel.Warning | LogLevel.Error,
    ["All"] = LogLevel.All
};

None не генерує жодних повідомлень, Warning | Error залишає лише одне попередження про дозволений прострочений сертифікат, а All додає трасу на кожному кроці. Сам підраховуючий логгер — це точка інтеграції для вашого стеку:

public void Warning(string message)
{
    Warnings++;
    WarningMessages.Add(message);
}

Реалізуйте ці три методи для Serilog, NLog або Application Insights, і діагностика бібліотеки потрапить туди, куди потрапляє решта ваших журналів.

Чи змінює рівень журналу, які виключення я отримую?

Ні, і варто це явно зазначити, бо два поняття виглядають пов’язаними. LogLevel фільтрує те, що доходить до ILogger. Виключення викидаються у ваш код у будь‑якому випадку: прострочений сертифікат без AllowExpired все одно викине виключення при LogLevel.None, і ваш блок catch працюватиме ідентично. Діагностика та контрольний потік — це окремі канали, що робить безпечним запуск у продакшн з Warning | Error.

Відхилення дешевше, ніж здається

Заперечення проти жорсткої зупинки є операційним: нічна партія, яка раніше успішно завершувалась, тепер падає о 02:00, і хтось отримує сповіщення. Це реальна вартість, і вона все ще менша. Відхилена партія — це одне сповіщення, одне оновлення сертифіката і один повторний запуск, все всередині вашої системи. Партія, підписана простроченим сертифікатом, виявляється одержувачем, що означає створення тікету підтримки, повторну випуску кожного ураженого документа і незручну розмову про те, як довго це тривало.

Приклад робить помилку конкретною, а не теоретичною: він навмисно підписує простроченим сертифікатом, ловить виключення і виводить повідомлення, тож ви бачите, що саме буде у ваших журналах до того, як оновлення потрапить у продакшн. Я б запустив цей метод проти вашого власного сховища сертифікатів перед плануванням підвищення версії.

Що робити перед оновленням

Три перевірки у порядку їхньої ймовірності:

  1. Перегляньте терміни дії сертифікатів у всіх шляхах підписання, включаючи ті, що запускаються щомісяця або щокварталу — саме там прострочений сертифікат ховається найдовше.
  2. Пройдіться пошуком HashAlgorithm: якщо ніде не встановлюється, ваші хеші зміняться з SHA‑1 на SHA‑256 після оновлення, що є поліпшенням і має бути зазначено у нотатках про випуск.
  3. Навмисно оберіть рівень журналу. Чесний варіант за замовчуванням для сервісу — Warning | Error; All використовується для відтворення конкретної проблеми, а None означає відмову від єдиного сигналу, який повідомляє, що підпис був створений за умов відмови.

Перевірка змінилася в тому ж напрямку

Легко пропустити, бо в коді виклику нічого не треба змінювати. DigitalVerifyOptions без встановлених критеріїв раніше був майже бездіяльним: він порівнював лише те, що йому передали, і коли нічого не передавали, сказати нічого не міг. З 26.9 той самий виклик виконує повну криптографічну перевірку кожного цифрового підпису PDF.

Для сервісу, який верифікує вхідні документи, це тихе оновлення з «тут є підпис» до «цей підпис відповідає цьому вмісту». Варто знати це до того, як ви побачите, що документ починає не проходити перевірку, яка проходила минулого місяця: документ, ймовірно, був змінений, а старий чек просто цього не помічав.

Сертифікати у прикладі

Один нюанс, який варто скопіювати, а не код: у прикладі немає приватного ключа. TestCertificates.cs створює три самопідписані PFX у пам’яті під час виконання — дійсний, прострочений минулого року, дійсний з наступного року — тому демонстрація працює незалежно від поточної дати, і в репозиторії немає нічого конфіденційного.

Такий підхід варто впровадити у власних тестових наборах. Закомічений тестовий сертифікат зрештою прострочиться, і коли це станеться, помилка виглядатиме точно так, як баг, який цей випуск мав виявити.

Висновок

Три зміни, один напрямок: помилки, які раніше з’являлися у одержувача, тепер з’являються у відправника. Оновлюйте сертифікат, а не використовуйте AllowExpired, робіть SHA‑256 стандартом, криптографічно перевіряйте вхідні документи і обирайте рівень журналу заздалегідь. Приклад виконує всі шість поведінок в одному проході, включаючи відхилення, тому оновлення можна відпрацювати за кілька хвилин.

Додаткові ресурси