💡 Повний робочий приклад доступний на 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 і фіксація версії залишилися ідентичними. Команди в охороні здоров’я, уряді та страховій сфері звертаються до цього рішення з тієї ж причини, що й фінанси: ключ має бути в місці, яке може довести, для чого його використано.
Перші кроки
- Почати безкоштовно: отримати пробну ліцензію, повний доступ до API
- Прочитати посібник з інтеграції: підписання PDF за допомогою ключа Vault Transit у Python
- Переглянути код: vault-transit-pdf-signing-python
- Задавати питання щодо реалізації на форумі спільноти