💡 完全な動作例は GitHub で入手可能です:
sign-docx-with-mldsa-certificates-python

はじめに

午後に RSA-2048 で契約書に署名すれば、その契約が有効である限り約束が維持されます。もしそれが 20 年か 30 年、そして権利書や同意書、エンジニアリングのサインオフなどでしばしばそうであるなら、その約束はアルゴリズムよりも長く持続しなければなりません。攻撃は今日存在する必要はありません。文書が意味を持たなくなる前に存在すればよく、その時点で公開鍵を持つ誰でも秘密鍵を導出し、あなたの名義で署名できてしまいます。

ポスト量子文書署名は、Python 用の GroupDocs.Signature 機能で、約束を ML-DSA(2024 年に NIST が FIPS 204 として標準化した署名アルゴリズム)に基づくものに置き換えます。Word 形式のサポートは GroupDocs.Signature 26.9 で追加され、既存の API をそのまま再利用します。ML-DSA 鍵は PFX に格納され、RSA 鍵と同様に DigitalSignOptions に渡します。

このガイドでは DOCX を 4 つの手順で署名し、測定結果で 3 つのセキュリティレベルを比較し、公開証明書だけで署名を検証し、導入前に知っておくべき 2 つの制限で締めくくります。

なぜ通常の移行よりも重要なのか

署名の移行は暗号化の移行とは異なる点があり、延期しやすく、修正が面倒です。

暗号化では「今すぐ取得して後で復号」問題が直ちに顕在化します。今日傍受されたデータは保存して後で開くことが可能です。署名の場合、既に署名したものが遡って偽造できることはありませんが、鍵が証明書から導出できるようになると、署名したものが自分のものと証明できなくなります。10 年分のアーカイブ文書を新しい鍵で再署名することは可能ですが、誰もそれを計画したがりません。

そのため実務的な助言は「広範に」ではなく「絞って」行うべきです。保存期間が長い文書だけを移行し、残りはそのままにします。すでに基準が設定されているプロファイルもあります—CNSA 2.0 は国家安全保障システムに ML-DSA-87 を要求しています—それ以外はファイルがどれだけ長期間防御可能である必要があるかが判断基準になります。

前提条件

  • 64 ビットインタプリタ上の Python 3.9 以降 — パッケージはバンドルされた .NET ランタイムを同梱し、32 ビット版の wheel は提供しません
  • GroupDocs.Signature for Python via .NET 26.10.0、評価制限を解除するための free temporary licence が必要です
  • パスワード保護された PFX 形式の ML-DSA 証明書、そして署名対象の 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() で調べた結果、subject が公開されていないと誤判断したことがあります。したがって、読み取る前に introspect すると、実際に存在する値を見逃してしまいます。

手順 2 - 3 つのセキュリティレベルを比較

ML-DSA には 3 つのパラメータセットがあり、異なる証明書を渡すことで選択します:

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 bytes -
ML-DSA-65 3 140,650 bytes +2,448 bytes
ML-DSA-87 5 143,971 bytes +5,769 bytes

最弱レベルと最強レベルの差は 6 KB 未満です。契約書にとってはほとんど差がないため、判断は簡単です。デフォルトは ML-DSA-65、カテゴリ 5 が要求されるプロファイルやサイズが問題とならない場合は ML-DSA-87、そしてキロバイト単位で多数のファイルを署名するようなケースでのみ 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

サンプルは同一ファイルに対して 2 回呼び出します。1 回目は署名鍵の公開部分である mldsa65.cer、2 回目は別の署名者の PFX を使用します。1 回目は True、2 回目は 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)

SignatureType.DIGITAL で search すると、証明書・署名時刻・有効性フラグを保持した 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 未満)に過度にこだわらない。
  • Word で受信者が検証する場合は RSA を残す。 読者がフラグを立てる正しい署名は、遅い移行よりも問題が大きいです。
  • テスト用証明書は差し替える。 サンプルの PFX は公開パスワード付きの自己署名証明書で、実際に署名しても何も証明しません。
  • 署名後に検証を実施 する。受信者が持つであろう公開証明書を使ってパイプライン内で検証してください。

よくある問題のトラブルシューティング

Microsoft Word が署名を有効と表示しない。 現時点では予想通りです。ML-DSA 用の標準 XML‑DSig 識別子が存在しないため、Word は正しい署名でも認識できないことがあります。自分のパイプラインで検証し、Word のインジケータに依存する文書は RSA を残してください。

署名呼び出しが PDF やスプレッドシートを受け付けない。 ML-DSA 署名は Word 系フォーマット(DOCX、DOC、ODT など)にのみ対応しています。PDF、スプレッドシート、プレゼンテーションはまだサポートされておらず、従来通り RSA または ECDSA で署名してください。

証明書の subject が空になる。 ほぼ常に手順 1の dir() トラップが原因です。属性は動的に解決されるため、先にテストせずに直接取得してください。

結論

コードの変更は「証明書の差し替え」だけで、緊急性が高まる前に実施すべきポイントです。長期保存が必要な Word 文書は ML-DSA-65 で署名し、プロファイルが要求する場合は ML-DSA-87 を使用し、公開証明書で検証し、フォーマットやリーダーが要求する場合は RSA を残します。

サンプルを自社の契約書のいずれかに実行すれば、3 つのサイズがバイト単位で示す最強レベルのコストが分かります。私がテストしたファイルでは 5,769 バイトでした。

追加リソース