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

Чому автоматичне підписання не проходить перевірку зберігання ключа

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

Зовнішнє підписання — це можливість GroupDocs.Signature для Python через .NET, яка передає дайджест документа у ваш код замість підпису приватним ключем, що зберігається в застосунку. Воно існує, бо сховище PFX не витримує ретельної перевірки. Файл — це копія, а копії розповсюджуються: у образ контейнера, у конвеєр розгортання, у резервну копію, у ноутбук розробника, який відлагоджував проблему у продакшені минулої весни. Будь-хто, хто має одну з цих копій і пароль, може підписувати від вашого імені, доки хтось не помітить і не відкличе сертифікат.

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

Чому файл сертифіката, захищений паролем, недостатній

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

Тут допомагає Transit‑движок Vault. Він виконує криптографію як сервіс: ви надсилаєте значення, він підписує або шифрує його своїм ключем і повертає результат. Ключі, створені всередині, можуть бути позначені як неекспортовані, і він ніколи не зберігає отримані дані. GroupDocs.Signature охоплює іншу частину, бо DigitalSignOptions.custom_sign_hash дозволяє підключити ваш власний підписувач до конвеєра підписання PDF.

Налаштування: GroupDocs.Signature для Python з Vault Transit

GroupDocs.Signature виконує все до криптографії. Він відкриває PDF, визначає, який діапазон байтів охоплює підпис, хешує цей діапазон і викликає ваш підписувач з дайджестом. Ваш підписувач передає дайджест у Vault і повертає отримані байти. Бібліотека вбудовує їх разом із сертифікатом і записує звичайний підписаний PDF.

Потрібні Python 3.11 або 3.13, пакети groupdocs-signature-net і asn1crypto, а також Docker для контейнера Vault у режимі розробки. Ліцензія GroupDocs необов’язкова; без неї режим оцінки дозволяє документи до 2‑х сторінок. Прикладний репозиторій працює від початку до кінця у два команди, а той самий код можна направити на продакшн‑Vault, змінивши дві змінні середовища.

Побудова крок за кроком

Крок 1 – Читання ключа підпису з Vault

Застосунок запитує у Vault ключ, який йому дозволено використовувати, і отримує номер версії та публічний ключ. Приватна частина ніколи не входить у відповідь.

key = vault.read_key(KEY_NAME)
if key is None:
    # First run: enable Transit and create an RSA-2048 key.
    vault.enable_transit()
    vault.create_key(KEY_NAME, "rsa-2048")
    key = vault.read_key(KEY_NAME)
print(f"Signing key: '{KEY_NAME}', version {key.version}")

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

Крок 2 – Створення сертифіката для ключа, який неможливо експортувати

Підпис PDF містить X.509‑сертифікат, щоб верифікатор міг знайти публічний ключ. При неекспортованому ключі сертифікат формується навколо публічної частини і підписується Vault.

tbs_certificate = x509.TbsCertificate({...})   # built around the Vault public key

# Hash locally, then have Vault sign the certificate's own digest.
digest = hashlib.sha256(tbs_certificate.dump()).digest()
signature = vault.sign_hash(key_name, key.version, digest, "sha2-256")

certificate = x509.Certificate({
    "tbs_certificate": tbs_certificate,
    "signature_algorithm": algorithm,
    "signature_value": signature,
})

Сертифікат навмисно починається на один день раніше, ніж зараз. PDF‑рідер порівнює час підпису з датою початку, і на машині, що знаходиться дві години східніше UTC, сертифікат, створений за секунди до підпису, був повідомлений Adobe Acrobat як прострочений або ще недійсний.

Крок 3 – Підпис PDF через зовнішній підписувач

certificate_object = DigitalVerifyOptions(
    certificate_stream=io.BytesIO(certificate)).certificate
digital_signature = DigitalSignature(certificate=certificate_object)
digital_signature.sign_time = datetime.now().astimezone()

options = DigitalSignOptions()
options.signature = digital_signature
options.custom_sign_hash = VaultSigner(vault, KEY_NAME, key.version)
options.hash_algorithm = HashAlgorithm.SHA256
options.reason = "Document signed with HashiCorp Vault Transit"

with gs.Signature(str(input_pdf)) as signature:
    signature.sign(str(output_pdf), options)

Два рядки специфічні для Python. У прив’язці немає типу X.509, тому об’єкт сертифіката повертається з DigitalVerifyOptions; передача шляху до DigitalSignOptions викликає помилку «Specified certificate doesn’t contain private key». А sign_time навмисно має часовий пояс, бо значення з офсетом записується у PDF.

Всередині підписувача дайджест надсилається у Vault з prehashed = true і pkcs1v15. Ці два поля визначають, чи підпис дійсний, і перше тихо провалюється, коли його немає.

Крок 4 – Перевірка перед виходом сервісу

options = DigitalVerifyOptions(certificate_stream=io.BytesIO(certificate))
with gs.Signature(str(signed_pdf)) as signature:
    is_valid = signature.verify(options).is_valid

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

Бізнес‑вплив: що змінюється для аудиту

Питання PFX на хості застосунку Підписання через Vault Transit
Копії приватного ключа одна на хост, образ, резервну копію та ноутбук одна, всередині Vault
Права, потрібні підписуючому застосунку повний доступ до файлу ключа читання ключа, виклик transit/sign
Доказ, що документ підписано журнали застосунку, якщо їх додали один запис аудиту Vault на підпис
Реакція на підозрюване витікання відкликання, переоформлення, повторне розгортання скрізь обертання ключа у Vault, одноразове переоформлення
Хто може підписати від вашого імені будь-хто, хто має файл і пароль будь-хто, хто має дійсний токен Vault

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

Реальний робочий процес: кредитна угода, підписана сервісом

Кредитна платформа генерує PDF‑угоду, коли заявка схвалена, підписує її і зберігає у сховищі документів клієнта. До змін сервіс підписання тримав сертифікат компанії, а інструкція розгортання містила крок копіювання PFX на нові хости. Після змін сервіс тримає токен AppRole з двома дозволами, а приватний ключ сертифіката ніколи не існував ніде, крім Vault.

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

Що варто налаштувати в першу чергу?

Обробка дайджесту. Неправильна назва алгоритму гучно провалюється, Vault відповідає 500 crypto/rsa: input must be hashed message, і це вимагає хвилини. Відсутність прапорця prehashed не викликає жодної помилки: Vault знову хешує дайджест, підпис охоплює неправильне значення, і PDF виглядає завершеним. Перевіряйте кожен файл, який підписуєте, і ця помилка перестане бути невидимою.

Інші випадки, де підходить цей дизайн

Хук не знає, що стоїть за ним, тому ті ж чотири кроки працюють з апаратним модулем безпеки, хмарним KMS або смарт‑карткою. Я переніс цю збірку зі свого .NET‑братчика, очікуючи, що підписувач потребуватиме переписування, і єдина частина, яка справді змінилася, — це передача сертифіката; тіло запиту до Vault і фіксація версії залишилися ідентичними. Команди в охороні здоров’я, уряді та страховій сфері звертаються до цього рішення з тієї ж причини, що й фінанси: ключ має бути в місці, яке може довести, для чого його використано.

Перші кроки

Ресурси