💡 Полный рабочий пример доступен на GitHub:
sign-docx-with-mldsa-certificates-python
Введение
Подпишите контракт сегодня после обеда с RSA‑2048, и вы дадите обещание, которое должно сохраняться столько, сколько важен контракт. Если это двадцать‑тридцать лет — а для актов, согласий и инженерных подписей это часто так — обещание должно превзойти срок действия алгоритма. Атака не обязана существовать уже сейчас. Она должна появиться до того, как документ перестанет иметь значение, и тогда любой, кто обладает публичным ключом, сможет вывести приватный и подписать от вашего имени.
Подписание документов в пост‑квантовом режиме — это функция GroupDocs.Signature для Python, заменяющая такое обещание на построенное на ML‑DSA, алгоритме подписи, стандартизированном NIST как FIPS 204 в 2024 году. Поддержка формата Word появилась в GroupDocs.Signature 26.9 и использует тот же API, что и раньше: ключ ML‑DSA хранится в PFX и передаётся в DigitalSignOptions точно так же, как RSA‑ключ.
В этом руководстве документ DOCX подписывается в четыре шага, сравниваются три уровня безопасности по измеренному размеру, проверяется подпись только с помощью публичного сертификата, и в конце рассматриваются два ограничения, о которых стоит знать перед внедрением.
Почему это важнее обычной миграции
Миграция подписи отличается от миграции шифрования тем, что её легче откладывать, но сложнее исправлять.
При шифровании проблема «собрать‑сейчас‑расшифровать‑позже» очевидна: любой перехваченный сегодня материал можно сохранить и открыть позже. При подписи ничего, что уже подписано, не становится подделываемым задним числом — но и подпись уже не доказывает, что документ действительно ваш, как только ключ может быть восстановлен из сертификата, копию которого имеет каждый. Переподписание десятилетних архивных документов новыми ключами возможно, и никто не хочет планировать такой процесс.
Поэтому практический совет узконаправлен, а не общий: мигрировать только те документы, срок хранения которых велик, остальные оставить как есть. Некоторые профили уже задали планку — CNSA 2.0 требует ML‑DSA‑87 для систем национальной безопасности — а для остальных решающим фактором является длительность, в течение которой файл должен оставаться защищённым.
Предварительные требования
- Python 3.9 или новее на 64‑битном интерпретаторе — пакет поставляется с включённым .NET‑runtime и не имеет 32‑битного колеса
- GroupDocs.Signature для Python через .NET 26.10.0, с бесплатной временной лицензией для снятия ограничений оценки
- Сертификат ML‑DSA в виде защищённого паролем PFX и документ Word для подписи
Установка
pip install groupdocs-signature-net
Шаг 1 — Подпись сертификатом ML‑DSA
Сертификат делает всю работу. Вызов такой же, как и для RSA:
with signature.Signature(source_path) as sign:
options = DigitalSignOptions(pfx_path)
options.password = CERTIFICATE_PASSWORD
result = sign.sign(output_path, options)
Это полностью покрывает сценарий адаптации кода, который уже подписывает: указываете DigitalSignOptions на другой PFX. Никаких новых параметров, отдельного указания алгоритма или ветки для пост‑квантового режима нет.
Чтение подписи обратно требует ещё один шаг и содержит единственную специфическую для Python ловушку в этом упражнении:
for created in result.succeeded:
certificate = getattr(created, "certificate", None)
subject = getattr(certificate, "subject", None)
if subject:
return str(subject)
Сертификат в объекте DigitalSignature — это мостовой объект, который динамически разрешает атрибуты. certificate.subject возвращает CN=GroupDocs.Signature MLDSA65 test, тогда как dir() того же объекта ничего не выводит. Я сначала проверил его через dir(), пришёл к выводу, что атрибут не доступен, и ошибся — поэтому, если вы делаете интроспекцию до чтения, пропустите существующее значение.
Шаг 2 — Сравнение трёх уровней безопасности
ML‑DSA поставляется в трёх наборах параметров, выбираемых передачей другого сертификата:
levels = (
("ML-DSA-44", MLDSA44_PFX),
("ML-DSA-65", MLDSA65_PFX),
("ML-DSA-87", MLDSA87_PFX),
)
for level, pfx_path in levels:
with signature.Signature(source_path) as sign:
options = DigitalSignOptions(pfx_path)
options.password = CERTIFICATE_PASSWORD
sign.sign(output_path, options)
sizes[f"{level} -> {file_name}"] = os.path.getsize(output_path)
Это шаг, который действительно стоит выполнить, потому что компромисс обычно описывается, но редко измеряется. На примере исходного контракта размером 132 KB:
| Уровень | Категория безопасности NIST | Подписанный файл | По сравнению с самым маленьким |
|---|---|---|---|
| ML-DSA-44 | 2 | 138 202 байт | - |
| ML-DSA-65 | 3 | 140 650 байт | +2 448 байт |
| ML-DSA-87 | 5 | 143 971 байт | +5 769 байт |
Разница менее 6 KB отделяет самый слабый уровень от самого сильного. Для контракта это ничто, что упрощает выбор: используйте ML‑DSA‑65 по умолчанию, ML‑DSA‑87 там, где профиль требует категорию 5 или где размер не важен, и ML‑DSA‑44 только когда вы подписываете столь большое количество файлов, что килобайты суммируются в значимый объём.
Шаг 3 — Проверка с публичным сертификатом
Получателю нужен только публичный сертификат подписанта и ничего секретного:
with signature.Signature(signed_path) as sign:
options = DigitalVerifyOptions(certificate_path)
if password is not None:
options.password = password
return sign.verify(options).is_valid
В примере этот вызов делается дважды для одного и того же файла: сначала с mldsa65.cer — публичной частью ключа подписи, затем с другим PFX подписанта. Первый вызов возвращает True, второй — False. Обратите внимание, что неверный сертификат даёт False, а не генерирует исключение — «подписано кем‑то другим» — это ответ, который ваш код должен обрабатывать, а не исключение. Проверка охватывает содержимое документа вместе с серийным номером и отпечатком сертификата, поэтому файл, изменённый после подписи, также не проходит.
Шаг 4 — Чтение подписей из документа
Когда приходит подписанный документ, и вы не знаете, какой сертификат ожидать:
with signature.Signature(signed_path) as sign:
found = sign.search(SignatureType.DIGITAL)
for item in found:
print(item.sign_time, item.is_valid)
search с SignatureType.DIGITAL возвращает объекты DigitalSignature, содержащие сертификат, время подписи и флаг валидности. Документ Word может содержать несколько подписей, включая смесь RSA и ML‑DSA, и каждая из них отображается со своим сертификатом и своей валидностью.
Меняется ли процесс проверки у получателей?
Никак. Получателю всё равно нужен только публичный сертификат подписанта, он передаёт его тем же DigitalVerifyOptions и получает булево значение. Никакая часть пути проверки не специфична для ML‑DSA. Единственное место, где алгоритм проявляется, — индикатор подписи в Microsoft Word, который пока может не распознавать ML‑DSA, поскольку у формата нет стандартного идентификатора.
Реальные сценарии применения
Долгосрочные контракты
Самый очевидный случай. Документ, который должен оставаться проверяемым десятилетиями, подписывается один раз сейчас с ML‑DSA‑65 или ML‑DSA‑87 и больше не требует переподписания, потому что алгоритм уже не устареет.
Регулируемые среды с заданным профилем
Когда применяется CNSA 2.0 или аналогичный профиль, уровень выбирается не по усмотрению — требуется ML‑DSA‑87, и единственный инженерный вопрос — поддерживается ли формат.
Смешанные конвейеры во время миграции
Подписание новых документов в пост‑квантовом режиме при оставлении архива без изменений — это полностью приемлемое промежуточное состояние, а отдельный вывод каждой подписи через search делает его управляемым.
Лучшие практики и рекомендации
- Мигрировать по сроку хранения, а не по объёму. Документы, требующие долгосрочного хранения, нуждаются в этом; квитанция, важная 90 дней, — нет.
- По умолчанию использовать ML‑DSA‑65, если профиль не задаёт уровень, и не переживать из‑за разницы в размере — она менее 6 KB на подпись.
- Оставлять RSA, если получатель проверяет в Word. Неправильные подписи, которые читатель помечает как ошибочные, хуже, чем более медленная миграция.
- Заменить тестовые сертификаты. PFX‑файлы в примере самоподписаны с опубликованным паролем, поэтому подпись ими ничего не доказывает.
- Проверять после подписи в любом конвейере, используя публичный сертификат, который будет у получателя.
Устранение распространённых проблем
Microsoft Word не показывает подпись как действительную. Ожидаемо пока: нет стандартного XML‑DSig идентификатора для ML‑DSA, поэтому Word может не распознать её, хотя подпись корректна и проверяется GroupDocs.Signature. Проверяйте в собственном конвейере и оставляйте RSA для документов, получатели которых полагаются на индикатор Word.
Вызов подписи отклоняется для PDF или таблицы. Подпись ML‑DSA охватывает форматы Word — DOCX, DOC, ODT и т.п. PDF, таблицы и презентации пока не поддерживаются, их следует подписывать RSA или ECDSA, как прежде.
Субъект сертификата возвращается пустым. Почти всегда это ловушка dir() из Шага 1: атрибут разрешается динамически, поэтому читайте его, а не проверяйте наличие заранее.
Заключение
Изменение кода сводится к изменению сертификата, что делает эту задачу полезной ещё до того, как она станет срочной. Подписывайте долгосрочные документы Word с ML‑DSA‑65, используйте ML‑DSA‑87, где это требует профиль, проверяйте подпись публичным сертификатом и оставляйте RSA, если формат или читатель требуют этого.
Запустите пример на одном из ваших контрактов, и три размера покажут вам в байтах, сколько стоит самый сильный доступный уровень. На тестовом файле это было 5 769 байт.