💡 完整可執行範例位於 GitHub:
sign-docx-with-mldsa-certificates-python

介紹

今天下午使用 RSA‑2048 簽署合約,即表示您作出了一個必須在合約仍具意義時持續有效的承諾。若這個期限是二三十年——對於契約、同意書與工程簽核而言常常如此——則此承諾必須超越演算法的壽命。攻擊不必在今天就存在,只要在文件失去意義之前出現即可,屆時任何持有公鑰的人都能推導出私鑰,冒用您的名義簽署。

後量子文件簽署是 GroupDocs.Signature for Python 的功能,將上述承諾以 ML‑DSA(2024 年 NIST 標準化為 FIPS 204)取代。Word 格式支援於 GroupDocs.Signature 26.9 版加入,且使用您已熟悉的 API:ML‑DSA 金鑰存於 PFX 中,並像 RSA 金鑰一樣放入 DigitalSignOptions。

本指南以四個步驟簽署 DOCX,比較三種安全等級的實測輸出,僅使用公證書驗證簽章,最後說明兩個在投入前必須了解的限制。

為何此議題比一般遷移更重要

簽章遷移與加密遷移不同之處在於,它更容易被延後,且更難修正。

對於加密而言,「現在截取‑‑‑之後解密」的問題是即時的:今天被攔截的資料可以被儲存,之後再解開。對於簽章而言,已簽署的文件不會因日後演算法被破解而變得可偽造——但一旦金鑰能從所有人都有的證書中推導出來,先前的簽章也不再能被證明是您的。重新使用新金鑰為十年存檔文件簽署是可行的,但沒有人願意成為規劃此事的人。

因此實務建議較為精準:只遷移保存期限長的文件,其餘保持不變。一些標準已設定門檻——CNSA 2.0 要求國家安全系統使用 ML‑DSA‑87——對其他情況而言,決策依據是文件需要保持可防禦的時間長短。

前置條件

  • Python 3.9 以上的 64 位元直譯器——套件內建 .NET 執行環境,且不提供 32 位元 wheel
  • 透過 .NET 26.10.0 取得 GroupDocs.Signature for Python,並使用免費暫時授權解除評估限制
  • 一把受密碼保護的 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 是一個橋接物件,屬性會動態解析。certificate.subject 會回傳 CN=GroupDocs.Signature MLDSA65 test,但對同一物件執行 dir() 卻什麼也不顯示。我最初先用 dir() 檢查,誤以為 subject 沒被公開,結果錯過了實際存在的值——因此若在讀取前先做 introspection,會錯過這個欄位。

步驟 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;若有檔案大小不重要或有規範要求第 5 類別,則改用 ML‑DSA‑87;只有在需要簽署大量檔案、累積的 KB 數才會成為考量時,才使用 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)

使用 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。 若讀者在 Word 中標記簽章為錯誤,會比遷移速度慢更糟。
  • 自行更換測試用證書。 範例的 PFX 為自行簽署且密碼已公開,任何使用它簽署的文件都不具備可信度。
  • 在任何流程中簽署後即驗證,使用收件者將會擁有的公證書。

常見問題排除

Microsoft Word 未將簽章顯示為有效。 目前預期的行為:Word 尚未支援 ML‑DSA 的標準 XML‑DSig 識別碼,因此即使簽章正確且 GroupDocs.Signature 已驗證通過,Word 仍可能無法辨識。請自行在流程中驗證,並對需要 Word 指示的文件保留 RSA。

簽署呼叫在 PDF 或試算表上被拒絕。 ML‑DSA 簽署目前僅支援 Word 系列格式(DOCX、DOC、ODT 等),PDF、試算表與簡報尚未支援,仍須使用 RSA 或 ECDSA。

證書的 subject 為空。 幾乎總是 Step 1 中的 dir() 陷阱:屬性是動態解析的,應直接讀取而非先測試是否存在。

結論

唯一的程式碼變更就是更換證書,這也是在需求緊迫之前就值得採取的步驟。使用 ML‑DSA‑65 簽署長期保存的 Word 文件,對於需要更高等級的情況使用 ML‑DSA‑87,使用公證書驗證,並在格式或讀者要求時保留 RSA。

將範例套用在您自己的合約上,三種大小會以位元組告訴您最強等級的成本。我的測試檔案增加了 5,769 位元組。

其他資源