💡 Полный рабочий пример доступен на 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 байт.

Дополнительные ресурсы