HotPDF 能驗證已載入 PDF 文件中的 ML-DSA-44、ML-DSA-65、ML-DSA-87、Ed25519 與 Ed448 CMS 簽章,並透過可插拔的提供者簽章,讓私鑰永遠不必存在於你的 Delphi 行程內。後半段是大多數團隊最先需要的部分。硬體權杖、遠端簽章服務與國民身分證卡都拒絕交出金鑰,而在簽章管線與金鑰存放分開之前,這些裝置一個都用不上
這種分離正是 THPDFSignatureProvider 的重點。HotPDF 留下它該擁有的部分——剖析 CMS、建構 SignedData、安排 /ByteRange——並把唯一一項它不能擁有的操作委派出去,也就是用一把它不被允許看見的金鑰,把摘要變成簽章。以下的一切都源自這條分工
為什麼一份有效的 ML-DSA 簽章會驗證失敗?
因為 HotPDF 會拒絕一份未宣告對應延伸功能的已載入文件上的 ML-DSA。ML-DSA——這個被標準化為 FIPS 204 的格狀簽章機制,也是人們說「後量子 PDF」時所指的東西——在 ISO 32000-2 中尚無註冊。一份帶有它的 PDF,用的是基礎標準未命名的演算法,而一份悄悄使用了未命名演算法的檔案,其判定結果沒有其他人能重現
所以 HotPDF 讓這項宣告變得明確。EnsureMLDSAExtensions 在允許的情況下把文件提升到 PDF 2.0,並把 /Extensions /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> 寫進 Catalog。在讀取端,LoadedDocumentDeclaresMLDSAExtension 會回報這項宣告是否留存,而 VerifyLoadedSignatureWithOptions 在接受 Options.AllowMLDSA 之前會先施加同樣的測試。在一份未宣告的文件上設定這個旗標,它會保持關閉——這個選項能放寬政策,卻永遠不能放寬結構要求
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'contract-pq.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
Pdf.EnsureMLDSAExtensions; // declare before the signature is written
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
要在儲存之前呼叫它,而不是之後。這項宣告是已簽署位元組範圍的一部分,事後才修補的 Catalog,要不是對已簽署檔案的一次未簽署變更,就是一個會被驗證器回報為修改的第二個修訂版
三種演算法家族,單一驗證入口
三個家族都經由 VerifyLoadedSignatureWithOptions 進入,它接受簽章索引、來源串流、一份 THPDFCMSVerifyOptions 記錄,以及一個用來回傳簽章細節的 out 參數。這份記錄只有三個欄位,而每一個都回答了過去需要重新建置才能回答的問題
SignatureProvider 用你自己的提供者取代內建的平臺提供者。OpenSSLLibraryPath 選擇一套 OpenSSL 3 函式庫,正是它提供了 Windows CNG 並非處處都有的純模式 Ed25519 與 Ed448 驗證。AllowMLDSA 選擇加入格狀演算法,但須通過上述延伸檢查。被辨識出來的確切演算法 OID 會透過 THPDFSignatureInfo.SignatureAlgorithmOID 傳回,所以稽核日誌可以記錄實際驗證了什麼,而不是請求驗證了什麼
var
Opts: THPDFCMSVerifyOptions;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
Src: TFileStream;
begin
Opts := THPDFCMSVerifyOptions.Default;
Opts.OpenSSLLibraryPath := 'C:\openssl3\libcrypto-3-x64.dll';
Opts.AllowMLDSA := Pdf.LoadedDocumentDeclaresMLDSAExtension;
Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyWrite);
try
Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
if Status = svValid then
Memo1.Lines.Add('signed with OID ' + string(Info.SignatureAlgorithmOID));
finally
Src.Free;
end;
end;
Ed25519 與 Ed448 不需要延伸宣告,因為 ISO 32000-2 已經承認它們。它們確實需要一套實作它們的提供者,在大多數 Windows 部署上,這意味著把 OpenSSLLibraryPath 指向一套你隨產品出貨並掌控的函式庫,而不是機器上剛好存在的那一套
簽章提供者實際承諾了什麼?
提供者只承諾一件事:給定一個請求,回傳一個狀態,並且在簽章時回傳位元組。THPDFSignatureProviderRequest 帶著演算法與其 OID、摘要 OID、PSS 鹽長度、輸入是訊息還是已計算好的摘要、輸入本身、公開金鑰或憑證、一個金鑰識別碼,以及一個操作識別碼。這份記錄裡沒有任何一項是 HotPDF 專屬的——它就是權杖驅動程式或簽章服務本來就在講的詞彙
函式庫隨附三種實作。THPDFCallbackSignatureProvider 包裹匿名方法,這是從既有的內部簽章常式,通往一份可用 PDF 簽章的最短路徑。THPDFRemoteSignatureProvider 包裹一個傳輸回呼,並帶有重試上限、取消登錄,以及對輸入與簽章大小的上限,所以一臺 hang 住的 HSM 不會變成一個 hang 住的應用程式。THPDFPKCS11SignatureProvider 針對一個由呼叫端擁有且已通過驗證的 PKCS#11 工作階段與私鑰 handle 序列化 RSA 操作——HotPDF 從不登入、從不看見 PIN、也從不關閉一個它沒有開啟的工作階段
var
Provider: THPDFRemoteSignatureProvider;
begin
Provider := THPDFRemoteSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
out Signature: TBytes): THPDFSignatureProviderStatus
begin
// POST Req.Input to the signing service; Req.KeyIdentifier selects the key
if PostToSigningService(Req.KeyIdentifier, Req.Input, Signature) then
Result := spsValid
else
Result := spsProviderError;
end,
3, // RetryLimit
1048576, // MaxInputBytes
65536); // MaxSignatureBytes
try
// hand Provider to the signing call
finally
Provider.Free;
end;
end;
為什麼狀態列舉有六個值,而不是一個布林值
THPDFSignatureProviderStatus 區分出 spsValid、spsInvalid、spsUnsupported、spsMalformed、spsProviderError 與 spsCancelled,把它們摺疊掉會讓你失去正確應對的能力。一份密碼學上錯誤的簽章(spsInvalid)是一次資安事件。一個提供者未實作的演算法(spsUnsupported)是部署缺口。一次傳輸失敗(spsProviderError)值得重試,而一次由使用者取消的權杖提示(spsCancelled)則完全不值得重試
簽章的規則很窄:簽章提供者只有在帶著非空簽章時才回傳 spsValid。驗證提供者回傳 spsValid 或 spsInvalid,而其餘四個值在兩條路徑上都保持獨立。如果你自己撰寫提供者,請抗拒把所有你不認得的東西都對應到 spsInvalid 的誘惑——這會把一個缺少的 DLL 變成一份客戶簽章遭到偽造的回報
簽章實際落在檔案的什麼位置
有兩個函式把提供者連到真實的 PDF 位元組。HPDFCMSBuildSignedDataWithProvider 從文件的 SHA-256 摘要建構出 detached CMS,當你的工作流程在他處計算摘要時,這是正確的入口。HPDFCMSSignPDFStreamWithProvider 簽署 PDF 串流中既有的簽章預留位置,並保留標準的 /ByteRange 管線,當簽章預留位置是由 HotPDF 自己安排時,這是正確的入口
保留這條管線,比聽起來更重要。/ByteRange 慣例——兩個跳過十六進位簽章視窗的範圍——是每一個驗證器第一個檢查的東西,而一條改寫了它的提供者路徑,無論密碼學多麼健全,都會破壞 PAdES 一致性。HotPDF 讓版面與內建簽章路徑保持完全一致,所以一份透過 PKCS#11 權杖簽署的文件,會用與從 PFX 檔案簽署的文件相同的簽章驗證程式碼通過驗證。至於位於演算法選擇之上的設定檔規則,請見 Delphi 中的 PAdES 基線簽章逐步解說;而對於早於這套提供者模型的 ECDSA 專屬編碼陷阱,請見 ECDSA CMS 驗證與 P1363 簽章格式的筆記
不會讓你的文件陷入進退兩難的遷移順序
後量子整備是一個時程問題,而不是一個開關。當今幾乎沒有任何已部署的 PDF 閱讀器會驗證 ML-DSA,所以一份只用它簽署的文件,從讀者的角度來看,就是一份帶著無法驗證簽章的文件。能在真實封存中存活的順序是:保留 RSA 或 ECDSA 作為驗證器將評判的簽章,在政策要求量子抗性證據之處加上延伸宣告與第二份 ML-DSA 簽章,並且只有在消費端系統跟上之後,才把主要簽章移過去
HotPDF 今天給你的是從同一份程式碼撰寫並驗證兩者的能力,而且演算法會誠實地記錄在檔案與驗證結果裡。HotPDF 是一套原生 VCL PDF 元件,支援 Delphi 與 C++Builder,沒有外部 PDF 執行階段,所以簽章與驗證路徑都隨你的可執行檔一起出貨,而不是依附在一旁——完整功能清單與試用下載請見 HotPDF Delphi PDF 元件頁