💡 完整可執行範例位於 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 位元組。