💡 完整可運作的範例可在 GitHub 上取得: vault-transit-pdf-signing-python
為何自動簽署會在金鑰保管審查中失敗
合規與財務營運團隊每天簽署成千上萬的文件:貸款協議、對帳單、政策手冊、稽核信函。幾乎沒有哪一份是由人手點擊按鈕完成的。服務會在排程下執行簽署,而附在這些文件上的憑證則帶有組織的名稱。這使得簽署金鑰成為公司最敏感的資產之一,且它通常以 PFX 檔案的形式與使用它的應用程式放在一起。
外部簽署是 GroupDocs.Signature 在 Python 透過 .NET 的一項功能,會將文件摘要傳遞給您提供的程式碼,而不是使用應用程式持有的私鑰簽署。之所以需要此功能,是因為 PFX 的做法無法經受審查。檔案本身就是一份副本,而副本會散佈到:容器映像、部署管線、備份、去年春天除錯生產問題的開發者筆記本電腦。任何持有這些副本以及密碼的人,都能以您的名義簽署,直到有人注意到並撤銷憑證為止。
這些團隊真正需要的不是不同的檔案格式,而是金鑰只能存在於唯一位置,且每一次使用都留下稽核紀錄,並且簽署服務能在不取得金鑰本身的情況下請求簽名。這正是 HashiCorp Vault 之類的金鑰管理器的用途,前提是文件庫能被指示委派加密工作。
為何僅靠受密碼保護的憑證檔案仍不足
直覺上會把 PFX 鎖起來:限制檔案權限、將密碼存放於祕密管理器、加密磁碟。每一步都有幫助,但都無法改變核心事實。簽署的那一刻,私鑰已被載入到程序記憶體中,記憶體轉儲、容器逃逸或不小心的日誌敘述仍可能洩漏。您真正想要的控制是「根本無法讀取的金鑰」,而不是「較難讀取的金鑰」。
Vault 的 Transit 引擎正提供這樣的功能。它以服務形式執行加密:您送出一個值,它使用內部持有的金鑰簽名或加密,並回傳結果。於其中建立的金鑰可以標記為不可匯出,且它永遠不會儲存您傳入的資料。GroupDocs.Signature 負責另一半,因為 DigitalSignOptions.custom_sign_hash 允許您將自訂簽署器插入 PDF 簽署流程。
設定:GroupDocs.Signature for Python 搭配 Vault Transit
GroupDocs.Signature 完成除加密之外的所有工作。它開啟 PDF、決定簽章覆蓋的位元組範圍、對該範圍做雜湊,然後將雜湊傳給您的簽署器。您的簽署器把雜湊送到 Vault,取得 Vault 產生的位元組後回傳。函式庫將它們與憑證一起嵌入,寫出一個正常的已簽署 PDF。
您需要 Python 3.11 或 3.13、groupdocs-signature-net 與 asn1crypto 套件,以及用於開發模式的 Vault Docker 容器。GroupDocs 授權是可選的;若沒有授權,評估模式允許最多 2 頁的文件。範例倉庫只需兩個指令即可完整執行,且只要變更兩個環境變數即可指向正式的 Vault。
逐步建置
步驟 1 - 從 Vault 讀取簽署金鑰
應用程式向 Vault 請求可使用的金鑰,並取得版本號與公鑰。私鑰部份永遠不會出現在回應中。
key = vault.read_key(KEY_NAME)
if key is None:
# 第一次執行:啟用 Transit 並建立 RSA-2048 金鑰。
vault.enable_transit()
vault.create_key(KEY_NAME, "rsa-2048")
key = vault.read_key(KEY_NAME)
print(f"Signing key: '{KEY_NAME}', version {key.version}")
只讀一次版本號正是此步驟的目的。憑證只包含一把公鑰,因此憑證與每一個簽章必須來自同一個版本。
步驟 2 - 為無法匯出的金鑰建立憑證
PDF 簽章會攜帶 X.509 憑證,以便驗證者取得公鑰。對於不可匯出的金鑰,憑證會以公鑰為基礎組裝,並由 Vault 簽署。
tbs_certificate = x509.TbsCertificate({...}) # 以 Vault 公鑰為基礎建構
# 先在本機雜湊,然後讓 Vault 簽署憑證本身的雜湊值。
digest = hashlib.sha256(tbs_certificate.dump()).digest()
signature = vault.sign_hash(key_name, key.version, digest, "sha2-256")
certificate = x509.Certificate({
"tbs_certificate": tbs_certificate,
"signature_algorithm": algorithm,
"signature_value": signature,
})
憑證會刻意設定為比現在早一天。PDF 閱讀器會比較簽署時間與開始日期,在 UTC 東部兩小時的機器上,Adobe Acrobat 會將「簽署前幾秒鐘鑄造」的憑證報告為已過期或尚未生效。
步驟 3 - 透過外部簽署器簽署 PDF
certificate_object = DigitalVerifyOptions(
certificate_stream=io.BytesIO(certificate)).certificate
digital_signature = DigitalSignature(certificate=certificate_object)
digital_signature.sign_time = datetime.now().astimezone()
options = DigitalSignOptions()
options.signature = digital_signature
options.custom_sign_hash = VaultSigner(vault, KEY_NAME, key.version)
options.hash_algorithm = HashAlgorithm.SHA256
options.reason = "Document signed with HashiCorp Vault Transit"
with gs.Signature(str(input_pdf)) as signature:
signature.sign(str(output_pdf), options)
有兩行是 Python 專屬的。綁定沒有 X.509 型別,因此必須從 DigitalVerifyOptions 取得憑證物件;若改為把路徑傳給 DigitalSignOptions 會拋出「Specified certificate doesn’t contain private key」的錯誤。而 sign_time 故意設為具時區資訊,因為 PDF 會寫入帶有偏移量的時間值。
在簽署器內部,雜湊會以 prehashed = true 與 pkcs1v15 送到 Vault。這兩個欄位決定簽章是否有效,若缺少第一個欄位會靜默失敗。
步驟 4 - 在服務離開前先驗證
options = DigitalVerifyOptions(certificate_stream=io.BytesIO(certificate))
with gs.Signature(str(signed_pdf)) as signature:
is_valid = signature.verify(options).is_valid
在同一次執行中同時簽署與驗證,能把那種靜默失敗轉為即時失敗。對於文件管線而言,這就是關卡:未通過驗證的檔案永遠不會進入檔案庫、對方或客戶入口網站。
業務影響:審計會看到什麼變化
| 關注點 | 應用主機上的 PFX | 透過 Vault Transit 簽署 |
|---|---|---|
| 私鑰的副本 | 每台主機、映像、備份與筆記本電腦各一份 | 唯一一份,存於 Vault 內 |
| 簽署應用程式需要的權限 | 完全存取金鑰檔案 | 讀取金鑰、呼叫 transit/sign |
| 文件已簽署的證據 | 應用程式日誌(若有人自行加入) | 每筆簽章都有一筆 Vault 稽核紀錄 |
| 發現疑似洩漏時的回應 | 撤銷、重新發行、在所有地方重新部署 | 在 Vault 中輪換金鑰,僅重新發行一次 |
| 誰可以以您的名義簽署 | 任何取得檔案與密碼的人 | 任何持有有效 Vault 令牌的人 |
最後一行是真相。此設計是把祕密搬移,而非移除:被盜的 Vault 令牌仍能請求簽名。不同之處在於令牌是短期的、可在單一位置撤銷、僅授權兩項操作,且每一次使用都會被記錄。相較於無法枚舉所有副本的檔案,這在審查時更容易防禦。
真實工作流程:服務簽署的貸款協議
貸款平台在申請核准後產生協議 PDF、簽署,然後放入客戶的文件保管庫。變更前,簽署服務持有公司憑證,部署手冊還有一步是把 PFX 複製到新主機。變更後,服務僅持有具兩項權限的 AppRole 令牌,憑證的私鑰從未出現在 Vault 之外的任何地方。
當稽核人員詢問上季簽署了多少文件、由誰簽署時,答案直接來自 Vault 的稽核裝置,而不必重建應用程式日誌。當團隊輪換金鑰時,只需在單一位置輪換,並為新版本發行新憑證。
我應該先把什麼做好?
雜湊處理。錯誤的演算法名稱會大聲失敗,Vault 會回傳 500 crypto/rsa: input must be hashed message,耗時約一分鐘。缺少 prehashed 旗標則不會產生任何錯誤:Vault 會再次雜湊該雜湊值,簽章覆蓋錯誤的資料,PDF 看起來仍然完成。務必驗證每一個簽署的檔案,讓這類失敗不再隱形。
其他適用此設計的情境
此掛鉤不關心背後是什麼,因此相同的四個步驟同樣適用於硬體安全模組、雲端 KMS 或智慧卡。我從 .NET 版移植此建置時,只需要重新實作憑證交接的部分;Vault 的請求內容與版本釘選完全相同。醫療、政府與保險等領域的團隊也因同樣的原因採用此方案:金鑰必須位於能證明其用途的地方。
開始使用
- 免費開始:取得試用授權,完整 API 存取
- 閱讀整合指南: 使用 Vault Transit 金鑰在 Python 中簽署 PDF
- 瀏覽程式碼: vault-transit-pdf-signing-python
- 在 社群論壇 提問實作相關問題