💡 Повний робочий приклад доступний на 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, і хтось отримує сповіщення. Це реальна вартість, і вона все ще менша. Відхилена партія — це одне сповіщення, одне оновлення сертифіката і один повторний запуск, все всередині вашої системи. Партія, підписана простроченим сертифікатом, виявляється одержувачем, що означає створення тікету підтримки, повторну випуску кожного ураженого документа і незручну розмову про те, як довго це тривало.
Приклад робить помилку конкретною, а не теоретичною: він навмисно підписує простроченим сертифікатом, ловить виключення і виводить повідомлення, тож ви бачите, що саме буде у ваших журналах до того, як оновлення потрапить у продакшн. Я б запустив цей метод проти вашого власного сховища сертифікатів перед плануванням підвищення версії.
Що робити перед оновленням
Три перевірки у порядку їхньої ймовірності:
- Перегляньте терміни дії сертифікатів у всіх шляхах підписання, включаючи ті, що запускаються щомісяця або щокварталу — саме там прострочений сертифікат ховається найдовше.
- Пройдіться пошуком
HashAlgorithm: якщо ніде не встановлюється, ваші хеші зміняться з SHA‑1 на SHA‑256 після оновлення, що є поліпшенням і має бути зазначено у нотатках про випуск. - Навмисно оберіть рівень журналу. Чесний варіант за замовчуванням для сервісу —
Warning | Error;Allвикористовується для відтворення конкретної проблеми, аNoneозначає відмову від єдиного сигналу, який повідомляє, що підпис був створений за умов відмови.
Перевірка змінилася в тому ж напрямку
Легко пропустити, бо в коді виклику нічого не треба змінювати. DigitalVerifyOptions без встановлених критеріїв раніше був майже бездіяльним: він порівнював лише те, що йому передали, і коли нічого не передавали, сказати нічого не міг. З 26.9 той самий виклик виконує повну криптографічну перевірку кожного цифрового підпису PDF.
Для сервісу, який верифікує вхідні документи, це тихе оновлення з «тут є підпис» до «цей підпис відповідає цьому вмісту». Варто знати це до того, як ви побачите, що документ починає не проходити перевірку, яка проходила минулого місяця: документ, ймовірно, був змінений, а старий чек просто цього не помічав.
Сертифікати у прикладі
Один нюанс, який варто скопіювати, а не код: у прикладі немає приватного ключа. TestCertificates.cs створює три самопідписані PFX у пам’яті під час виконання — дійсний, прострочений минулого року, дійсний з наступного року — тому демонстрація працює незалежно від поточної дати, і в репозиторії немає нічого конфіденційного.
Такий підхід варто впровадити у власних тестових наборах. Закомічений тестовий сертифікат зрештою прострочиться, і коли це станеться, помилка виглядатиме точно так, як баг, який цей випуск мав виявити.
Висновок
Три зміни, один напрямок: помилки, які раніше з’являлися у одержувача, тепер з’являються у відправника. Оновлюйте сертифікат, а не використовуйте AllowExpired, робіть SHA‑256 стандартом, криптографічно перевіряйте вхідні документи і обирайте рівень журналу заздалегідь. Приклад виконує всі шість поведінок в одному проході, включаючи відхилення, тому оновлення можна відпрацювати за кілька хвилин.