💡 完整的工作範例可在 GitHub 上取得: digital-signing-certificate-validity-dotnet
合規問題:在審計員發現之前無人察覺
簽署服務運行了三年且未出現錯誤。文件發出,收件人接受,日誌中沒有任何問題的跡象。然後,對方的驗證器將一批文件標記為無效,調查發現兩個原因:簽名使用了 SHA‑1,且在過去四個月內憑證已過期。
這兩個失敗在簽署時都是沉默的。這正是 GroupDocs.Signature 26.9 所改變的地方。
為何沉默的成功代價高昂
簽署的特點在於,犯錯的一方並非發現錯誤的一方。格式錯誤的發票會在自己的系統中失敗;無效的簽名則在別人的系統中幾週後失效,且沒有可讀的診斷資訊。
正是這種不對稱,使得「API 回傳成功」在此並不是可靠的保證。舊的預設值優化為不打斷呼叫端,成本則落在收件端,最終落在需要重新簽署並重新發送數百份文件的人身上。
變更 1:過期憑證將被拒絕
這是最重要的變更。Sign 現在會在憑證的有效期已結束或尚未開始時拋出 GroupDocsSignatureException,且不會寫入磁碟。
try
{
signature.Sign(outputPath, options);
return true;
}
catch (GroupDocsSignatureException ex)
{
Console.WriteLine($" Rejected: {ex.Message}");
return false;
}
訊息會列出憑證名稱以及導致問題的屬性,讓讀取日誌的操作員無需打開文件說明即可採取行動。對於升級到 26.9 後開始失敗的管線,這幾乎總是原因所在——正確的回應是續期,而不是抑制。
當你真的需要舊行為時,只需設定一個屬性:
var options = new DigitalSignOptions(certificate)
{
Password = certificatePassword,
AllowExpired = true
};
文件仍會被簽署,且會向記錄器發出警告。驗證器仍會拒絕結果,因為 AllowExpired 控制的是程式庫允許的行為,而非憑證本身的價值。伴隨的旗標 AllowNotYetValid 覆蓋時間窗口的另一端,且刻意保持獨立:允許過期憑證並不會悄悄允許未來生效的憑證。
變更 2:預設使用 SHA-256
PDF 數位簽章現在使用 SHA‑256 以 adbe.pkcs7.detached 格式寫入,這是目前驗證器所期望的。早期版本使用的是 SHA‑1。
var options = new DigitalSignOptions(certificate)
{
Password = certificatePassword,
HashAlgorithm = HashAlgorithm.Sha256,
Reason = "Approved",
Location = "Head office"
};
僅在需要更高等級時才必須明確設定屬性——例如政策要求 Sha384 或 Sha512,或為了兼容只能接受 Sha1 的驗證器而保持使用 Sha1。時間戳記也會使用相同的雜湊演算法。
驗證在同一版本中也朝相同方向變更:未設定任何條件的 DigitalVerifyOptions 以前幾乎不執行任何操作,現在會執行完整的密碼學檢查,因而在簽署後被修改的文件會被報告為無效。
變更 3:LogLevel 真正過濾訊息
SignatureSettings 長期以來已接受記錄器。26.9 之前,等級會被忽略,導致所有訊息都會送出,多數服務為了避免被大量追蹤訊息淹沒而關閉日誌。
範例透過使用計數記錄器三次簽署同一文件,使差異可量測:
var levels = new Dictionary<string, LogLevel>
{
["None"] = LogLevel.None,
["Warning | Error"] = LogLevel.Warning | LogLevel.Error,
["All"] = LogLevel.All
};
None 產生零條訊息,Warning | Error 只保留因允許過期憑證而產生的單一警告,All 則在每一步加入追蹤訊息。計數記錄器本身即是你自有堆疊的整合點:
public void Warning(string message)
{
Warnings++;
WarningMessages.Add(message);
}
將這三個方法對接 Serilog、NLog 或 Application Insights,即可讓程式庫的診斷資訊落在服務其餘日誌所在的位置。
日誌等級會改變我收到的例外嗎?
不會,且值得明確說明,因為兩者看起來相關。LogLevel 只過濾到達 ILogger 的訊息。例外會無論日誌等級如何都拋給你的程式碼:即使在 LogLevel.None 下,未設定 AllowExpired 的過期憑證仍會拋出例外,且你的 catch 區塊行為相同。診斷與控制流程是分離的管道,這正是讓在生產環境使用 Warning | Error 安全的原因。
拒絕的成本比看起來更低
對硬性停止的反對主要是操作層面的:原本能在夜間批次完成的工作,現在在 02:00 失敗,導致有人被叫醒。這確實是一筆成本,但仍是較小的那筆。一次被拒絕的批次只會產生一次警報、一次續期與一次重新執行,全部發生在自己的系統內。相較之下,使用過期憑證簽署的批次被收件人發現,則會產生支援工單、所有受影響文件的重新發行,以及關於問題持續時間的尷尬對話。
範例將失敗具體化而非抽象化:它故意使用過期憑證簽署,捕獲例外並印出訊息,讓你在升級到正式環境前就能看到日誌會包含什麼內容。建議在排程版本升級前,先對自己的憑證庫執行此方法。
升級前的準備工作
三項檢查,依可能造成問題的概率排序。
- 檢查所有簽署路徑的憑證到期情況,包括每月或每季執行的路徑——這些路徑最容易讓過期憑證長時間隱藏。
- 搜尋
HashAlgorithm:如果沒有任何設定,升級後你的雜湊演算法會從 SHA‑1 變為 SHA‑256,這是一項改進,仍需寫入發行說明。 - 有意識地決定日誌等級。服務的誠實預設應為
Warning | Error;All用於重現特定問題,None則放棄唯一能告訴你簽章是在豁免條件下完成的訊號。
驗證也朝相同方向變更
這點容易被忽略,因為呼叫端的程式碼不需要變更。未設定任何條件的 DigitalVerifyOptions 以前幾乎等同於不執行任何操作:它只比較給定的條件,若沒有條件則無話可說。從 26.9 起,同樣的呼叫會對每個 PDF 數位簽章執行完整的密碼學檢查。
範例中的憑證
值得複製的細節不是程式碼本身:範例不會隨附私鑰。TestCertificates.cs 在執行時於記憶體中建立三個自簽 PFX——有效、去年過期、明年起有效——因此示範能在任何日期執行,且倉庫中不含任何敏感資訊。
這種模式值得在自己的測試套件中採用。提交的測試憑證最終會過期,屆時失敗正好會呈現本次發行欲揭露的錯誤。
結論
三項變更,同一方向:原本在收件端出現的失敗現在會在寄件端顯現。應以續期取代使用 AllowExpired,讓 SHA‑256 成為預設,對進來的文件執行密碼學驗證,並在需要之前先決定日誌等級。範例一次執行全部六種行為,包括拒絕,讓升級可在數分鐘內完成演練。