💡 Повний робочий приклад доступний на GitHub:
pdf-signing-certificate-checks-python

Вступ

Сервіс підписує завантажені PDF‑файли щовночі. Одного ранку сертифікат, яким він користується, перестає бути дійсним, і, здавалося б, нічого не змінюється: завдання виконується, файли записуються, журнал виглядає нормально. Через кілька тижнів хтось відкриває один із цих документів у Acrobat і бачить попереджувальний банер, бо підпис, створений за допомогою простроченого сертифіката, не є «слабшим» підписом — він вважається недійсним. Документи, які виглядають схваленими, вартують менше, ніж підписані без підпису, бо люди їм довіряли.

Ця відмова має назву. Перевірка дійсності сертифіката — це поведінка GroupDocs.Signature для Python, яка відмовляє у підписанні, коли термін дії сертифіката минув або ще не настав. Вона з’явилася у версії 26.9 разом із двома змінами того ж типу: SHA‑256 став типовим дайджестом для PDF‑підписів, а SignatureSettings.log_level почав фільтрувати повідомлення замість того, щоб тихо ігноруватись. Кожна з цих змін перетворює те, що раніше відбувалося непомітно, у явний результат.

У цій статті порівнюються ці три керування, коли вони застосовуються з Python через .NET — що саме змінює кожне, коли їх варто використовувати і які два нюанси прив’язки вартували мені полудня. Усі наведені результати отримані під час запуску прикладу на односторінковому PDF.

Чому це важливіше, ніж нотатка про версію

Три зміни мають спільну властивість, яку варто назвати: усі вони перетворюють помилку, яку ви б виявили пізніше, у помилку, яку ви бачите одразу.

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

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

Передумови

Перш ніж розпочати, переконайтеся, що у вас є:

  • Python 3.9 або новіший на 64‑бітному інтерпретаторі — пакет постачається з вбудованим .NET‑runtime і не має 32‑бітного колеса.
  • GroupDocs.Signature для Python через .NET 26.10.0, з безкоштовною тимчасовою ліцензією, якщо хочете зняти обмеження оцінки.
  • PDF‑файл для підпису та пакет cryptography, якщо плануєте створювати одноразові тестові сертифікати, як у прикладі.

Встановлення

pip install groupdocs-signature-net cryptography

Керування 1 — Дайджест, записаний у підпис

hash_algorithm у DigitalSignOptions вибирає дайджест. Типовим значенням з 26.9 є SHA‑256 у форматі adbe.pkcs7.detached, який очікують сучасні валідатори; раніше нові підписи були SHA‑1.

with signature.Signature(source_path) as sign:
    options = DigitalSignOptions()
    options.certificate_stream = io.BytesIO(pfx)
    options.password = PASSWORD
    options.hash_algorithm = HashAlgorithm.SHA512
    options.reason = "Approved"

    result = sign.sign(output_path, options)
    return len(result.succeeded)

Два нюанси варто підкреслити. Сертифікат передається через certificate_stream як io.BytesIO, а не шлях до файлу — так PKCS#12, створений у пам’яті, потрапляє до бібліотеки без запису на диск; приклад саме так робить, щоб не розповсюджувати приватний ключ. HashAlgorithm пропонує AUTO, SHA1, SHA256, SHA384 і SHA512; якщо ви додаєте мітку часу, вона використовує той же дайджест, що й підпис.

На практиці це найрідше змінюване керування. Типове значення вже правильне, SHA384 і SHA512 існують для випадків, коли політика підпису їх вимагає, а SHA1 — це режим сумісності для валідаторів, які не можна змінити.

Керування 2 — Чи зупиняє прострочений сертифікат підпис

Якщо не вказати нічого додаткового, підпис з сертифікатом, термін дії якого закінчився (або ще не настав), піднімає GroupDocsSignatureException і нічого не записує.

try:
    sign.sign(output_path, options)
    return True
except signature.GroupDocsSignatureException as error:
    print(f"Rejected: {str(error).splitlines()[0]}")
    return False

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

Коли дійсно потрібно підписати будь‑яко — тестування з архівним сертифікатом або пакет, який має виконатися сьогодні ввечері, поки оновлення ще в процесі — можна перевизначити поведінку під час виклику:

settings = signature.SignatureSettings(ConsoleLogger())
settings.log_level = LogLevel.WARNING | LogLevel.ERROR

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    result = sign.sign(output_path, options)

allow_not_yet_valid працює аналогічно для сертифіката, виданого на майбутню дату, і обидва прапорці незалежні: дозволити прострочений сертифікат не означає дозволити ранній. Ранній сертифікат зазвичай вказує на неправильний системний час, а не на аномалію сертифіката; неправильний час ставить під сумнів усі підписи, створені машиною, тому перевірте його перед будь‑яким перевизначенням.

Обидва перевизначення генерують попередження замість тихого успіху, що і є зв’язком із третім керуванням.

Керування 3 — Чи хтось дізнається

SignatureSettings.log_level — це значення‑прапорець. Приклад підписує один і той самий документ тричі, використовуючи LogLevel.NONE, LogLevel.WARNING | LogLevel.ERROR і LogLevel.ALL, підраховуючи, що саме приходить у журнал:

logger = CollectingLogger()
settings = signature.SignatureSettings(logger)
settings.log_level = level

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    sign.sign(output_path, options)

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

Два нюанси прив’язки вартували мені полудня, тому їх варто назвати відкрито. SignatureSettings.logger лише для читання, тому його передають у конструктор, а спроба присвоїти викликає AttributeError; log_level встановлюється звичайним способом після створення. Крім того, кастомний логер не повинен успадковувати groupdocs.signature.logging.ILogger — цей базовий клас обгортає нативний об’єкт, конструктор якого потребує дескриптора, яким володіє бібліотека, тому успадкування викликає TypeError. Прив’язка приймає будь‑який простий об’єкт, що реалізує три методи:

class StdlibLogger:
    def error(self, message, exception=None):
        logging.getLogger("groupdocs").error(message, exc_info=exception)

    def warning(self, message, exception=None):
        logging.getLogger("groupdocs").warning(message)

    def trace(self, message):
        logging.getLogger("groupdocs").debug(message)

Методам error і warning можна передавати необов’язковий параметр exception. Бібліотека не завжди його передає, і логер, який вимагає його, зламається на повідомленнях без цього параметра.

Порівняння трьох керувань: коли використовувати яке

Керування Краще підходить для Ключові переваги Обмеження
hash_algorithm виконання політики, що називає конкретний дайджест одне налаштування; однаковий розмір вихідного файлу безглуздо, якщо сам сертифікат недовірений
перевірка дійсності та перевизначення підписання документів для інших людей помилка з’являється там, де її можна виправити перевизначення створює файл, який не є довіреним
log_level сервіси з уже завантаженими журналами одиннадцять повідомлень стає одним лише фільтрує журнал, ніяких виключень не приховує

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

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

Ні. Він лише визначає, які повідомлення потрапляють у ваш логер, і нічого більше. Протермінований сертифікат все одно піднімає GroupDocsSignatureException при LogLevel.NONE, а allow_expired все одно підписує при LogLevel.ALL; значення повернення та виключення однакові для всіх рівнів. Єдине, що змінюється, — чи буде попередження про сумнівний підпис прочитане людиною.

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

Варто згадати, бо це інша половина того ж релізу. verify з порожнім DigitalVerifyOptions тепер криптографічно перевіряє кожен цифровий підпис PDF, тому документ, змінений після підпису, стає недійсним, а не просто «непоясненим»:

with signature.Signature(signed_path) as sign:
    result = sign.verify(DigitalVerifyOptions())
    return result.is_valid

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

Кращі практики та поради

  • Залишайте відмову за замовчуванням у всьому, що підписує від імені користувачів, і перевизначайте лише на рівні виклику, а не глобально. Виключення дешеве; пакет недійсних підписів — ні.
  • Логуйте текст попередження, а не лише лічильник. У ньому вказано сертифікат і дату, що є єдиною частиною, на яку оператор може реагувати.
  • Перевіряйте системний час перед дозволом «ще не дійсного» сертифіката. Зазвичай саме час неправильний, а не сертифікат, і це впливає на більше, ніж один виклик підпису.
  • Вимикайте трасування у продакшн. Приблизно десять повідомлень на запуск швидко накопичуються; вмикайте їх лише під час діагностики.
  • Перевіряйте після підпису у будь‑якому конвеєрі, тепер коли перевірка криптографічна, щоб виявити пошкоджений вихід до того, як його отримає одержувач.

Висновок

Три керування, один виклик підпису і одна спільна ідея дизайну: ризикований результат тепер вимагає рішення, а безпечний — нічого. Залишайте перевірку дійсності, розглядайте allow_expired як виключення на рівні виклику, яке ви логеруєте, не змінюйте дайджест, якщо політика не вимагає іншого, і встановлюйте рівень журналу, який робить попередження читабельними.

Запуск прикладу на вашому PDF займе хвилину і виведе саме те, що змінило кожне керування — шість підписаних файлів, одну навмисну відмову та три рядки підрахунків повідомлень, які більше не однакові.

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