帶著 ML-DSA-65 簽章的 PDF,可以通過產出它的工具鏈自己的檢查,卻在一個堅持檔案必須聲明「哪個延伸定義了這個演算法」的驗證器那裡得到不支援演算法的答覆。公鑰 1,952 位元組,恰好是 FIPS 204 為該參數集定的尺寸。簽章位元組完好,位元組範圍覆蓋整份檔案。這種驗證器要找的,是文件 Catalog 裡指名那個延伸的一條條目,因為 ISO 32000-2 根本沒有列出 ML-DSA
簽章這一側的牆不一樣。硬體權杖、雲端 HSM 或國民 eID 卡都願意按要求算一張簽章,但沒有一個會把私鑰交給你的應用程式,所以任何期待載入 PFX 檔、行程內簽章的簽署程式碼,根本無物可載。兩個問題都關乎決定住在哪裡:演算法宣告住在檔案裡,金鑰住在你的 Delphi 程式碼摸不到的地方
為什麼有效的 ML-DSA 簽章會換來不支援?
HotPDF 只有在載入的文件於 Catalog 中聲明了 HotPDF 開發者延伸時,才驗證 ML-DSA-44、ML-DSA-65 與 ML-DSA-87 CMS 簽章。沒有那條聲明,驗證器就對格狀簽章回傳 svUnsupportedAlgorithm,密碼學本身再健全也一樣。道理是可重現性:一份使用基礎標準未命名演算法、又不說明自己依賴哪個延伸的檔案,得出的判定,別的實作沒有依據可以重現
產出這一側,THotPDF.EnsureMLDSAExtensions 把 /Extensions << /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> >> 寫進 Catalog,文件若還停在較舊版本就一併升到 PDF 2.0。讀取這一側,LoadedDocumentDeclaresMLDSAExtension 只在該字典存在、/BaseVersion 等於 2.0 且 /ExtensionLevel 至少為 1 時回傳 True。VerifyLoadedSignatureWithOptions 把兩件事合起來:它拿呼叫端的 AllowMLDSA 旗標,在任何驗證開跑之前先與聲明檢查做 AND,所以在未聲明的檔案上打開旗標什麼也不會改變
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'contract-pq.pdf';
Pdf.StrictVersionLock := False; // 預設值;讓這次呼叫能把檔案升到 PDF 2.0
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
Pdf.EnsureMLDSAExtensions; // 在任何簽章蓋住 Catalog 之前聲明
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
那段程式裡有兩個細節有分量。呼叫必須發生在文件儲存與簽署之前,因為 Catalog 坐在已簽署位元組範圍裡;事後補寫,要嘛是對已簽署檔案的一次未簽署變更,要嘛是一個任何驗證器都會列為修改的新的增量修訂。第二個細節是 StrictVersionLock。鎖打開、文件被釘在 PDF 2.0 以下時,EnsureMLDSAExtensions 什麼也不寫就返回,因為 PDF 2.0 的延伸無法在 1.7 檔案裡聲明。若你的產生器為了封存理由釘死版本,依賴聲明之前先確認 Version = pdf20
EdDSA 與 ML-DSA 共用一個驗證呼叫
VerifyLoadedSignatureWithOptions 接收簽章索引、來源串流、一份 THPDFCMSVerifyOptions 記錄與一個 out THPDFSignatureInfo。記錄從 THPDFCMSVerifyOptions.Default 起步:AllowMLDSA 關著、CMS 容器上限 256 個簽署者、副簽章巢狀深度上限 8。對新演算法要緊的是三個欄位。SignatureProvider 讓你自己的驗證器優先作答。OpenSSLLibraryPath 指名執行純模式 Ed25519 與 Ed448 驗證的 OpenSSL 3 libcrypto。AllowMLDSA 選入格狀演算法,仍受前述 Catalog 檢查約束。記錄還帶簽章政策釘選欄位(RequireSignaturePolicy、ExpectedSignaturePolicyOID、ExpectedSignaturePolicyHash),對每個演算法家族都適用
內建路徑按演算法分家。Ed25519 與 Ed448 走 OpenSSL 3;沒給路徑時,平台提供者在 Win64 上按正常 DLL 搜尋順序嘗試 libcrypto-3-x64.dll、libcrypto-3.dll 與 libcrypto.dll。ML-DSA 走 Windows CNG 的 ML-DSA 演算法提供者,Windows 版本不提供它時結果就是不支援。把 OpenSSLLibraryPath 指向你隨執行檔出貨的那份。留空,判定就取決於哪個安裝程式碰巧在搜尋路徑上丟了哪個 libcrypto,這種事在稽核裡很難解釋
你自己的 SignatureProvider 先被問到。它回傳 spsUnsupported 時,內建平台提供者會接到同一個請求,所以只懂 ML-DSA 的提供者可以擋在預設 EdDSA 處理前面,而不必重寫那套處理
var
Pdf: THotPDF;
PQ: THPDFCallbackSignatureProvider;
Opts: THPDFCMSVerifyOptions;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
Src: TFileStream;
begin
PQ := THPDFCallbackSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest): THPDFSignatureProviderStatus
begin
if not (Req.Algorithm in [hsaMLDSA44, hsaMLDSA65, hsaMLDSA87]) then
Exit(spsUnsupported); // 把 EdDSA 交給平台提供者
// MyFips204Verify 是你自己包的 ML-DSA 程式庫轉接層;
// Req.Input 放著 DER 編碼的 signed attributes(純模式)
Result := MyFips204Verify(Req.AlgorithmOID, Req.PublicKey,
Req.Input, Req.Signature);
end);
Pdf := THotPDF.Create(nil);
Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyNone);
try
Pdf.LoadFromFile('contract-pq.pdf');
Opts := THPDFCMSVerifyOptions.Default;
Opts.SignatureProvider := PQ;
Opts.OpenSSLLibraryPath := ExtractFilePath(ParamStr(0)) + 'libcrypto-3-x64.dll';
Opts.AllowMLDSA := True; // 仍會與 Catalog 聲明做 AND
Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
if Status = svValid then
Memo1.Lines.Add(string(Info.SignatureAlgorithm) + ' / ' +
string(Info.SignatureAlgorithmOID));
finally
Src.Free;
Pdf.Free;
PQ.Free; // 選項記錄不擁有它
end;
end;
記錄 Info.SignatureAlgorithmOID,而不是你設定檔裡以為的那個演算法。ML-DSA-65 對應 2.16.840.1.101.3.4.3.18,一份記下檔案中實際找到之 OID 的稽核軌跡,日後能回答設定值回答不了的問題。完全不需要自訂提供者的話,較短的 VerifyLoadedSignature(SigIndex, Source, Info) 超載會建好預設選項,並替你從 Catalog 聲明設定 AllowMLDSA
讓外部產出的 ML-DSA 與 EdDSA CMS 踩坑的編碼規則
兩個演算法家族都以純模式對完整的 DER 編碼 signed attributes 驗證,沒有 signed attributes 的 SignerInfo 回報為不支援。EdDSA 的 AlgorithmIdentifier parameters 必須缺席。手搓 CMS 常見的錯,是先用 SHA-512 或 SHAKE256 雜湊 signed attributes 再簽那個摘要,這悄悄把 PureEdDSA 變成 CMS 裡無人聲明的 prehash 變體。ML-DSA 的驗證器還檢查參數集與訊息摘要的配對:ML-DSA-44 接受 SHA-256、SHA-384、SHA-512 或 SHAKE256,ML-DSA-65 要 SHA-384 或更強,ML-DSA-87 要 SHA-512 或 SHAKE256。配對錯了會出現在 Info.DigestConsistencyIssue,別家工具鏈產出的檔案驗不過時,先讀這個欄位
EdDSA 有自己的註冊故事。ISO/TS 32002 是為 PDF 2.0 簽章註冊 Ed25519 與 Ed448 的延伸,EnsureISO32002Extensions 把對應的 ISO_ 條目連同 ExtensionLevel 32002 加進 Catalog。驗證在沒有該條目時也接受 EdDSA,但你自己產出的檔案仍應帶著它,理由與 ML-DSA 聲明一樣:可重現性
簽章提供者被要求做什麼
簽章提供者收到一個 THPDFSignatureProviderRequest,回傳一個 THPDFSignatureProviderStatus,成功時外加簽章位元組。請求帶著演算法與其 OID、摘要演算法 OID、PSS 鹽長度、Input 是訊息(sikMessage)還是預先算好的摘要(sikDigest)、輸入位元組、簽署者憑證、金鑰識別碼,以及這次呼叫唯一的操作識別碼。這套詞彙權杖驅動程式與簽章服務本來就會說,而且沒有一項需要金鑰跨進你的行程
每個提供者還公告一組能力,例如 spcSign、spcCancel、spcHardwareKey 與 spcRemoteKey。程式庫隨附幾種實作:
THPDFCallbackSignatureProvider用匿名方法包住驗證、簽章與取消,是從既有內部簽章常式通往 PDF 簽章的最短路THPDFRemoteSignatureProvider包住一個傳輸函式,帶重試上限、取消登錄簿,以及輸入與簽章尺寸上限,行為失常的遠端服務沒辦法把超大回應塞進 CMS 編碼器THPDFPKCS11SignatureProvider借用你的程式碼已經開啟並登入的 session 與私鑰 handle。HotPDF 永遠看不到 PIN,也不管理權杖 session;它把C_Sign呼叫序列化、為 RSA PKCS#1 v1.5 前置 DigestInfo、透過 mechanism 參數把真正的鹽長度交給 RSA-PSS,並把原始 ECDSAr||s結果轉成 CMS 期待的 DER 形式THPDFWindowsKeyStorageSignatureProvider透過 Windows CNG 金鑰儲存簽章,涵蓋智慧卡與 TPM 支撐的金鑰,並可要求硬體支撐的金鑰
PKCS#11 提供者收摘要輸入,所以處理 RSA PKCS#1 v1.5、RSA-PSS 與 ECDSA。Ed25519 與 Ed448 簽的是完整的 signed-attributes 訊息,那要經過 callback 或遠端提供者,由你的程式碼把 Req.Input 當訊息交給權杖或服務
var
Provider: THPDFRemoteSignatureProvider;
Opts: THPDFCMSSignOptions;
CertDER: TBytes;
InStream, OutStream: TFileStream;
begin
CertDER := TFile.ReadAllBytes('signer.cer'); // System.IOUtils;遠端金鑰的 DER 憑證
Provider := THPDFRemoteSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
out Signature: TBytes): THPDFSignatureProviderStatus
begin
// PostToSigningService 是你自己的 HTTP 用戶端:把逾時與 5xx 映射成
// spsProviderError、被拒絕的核可映射成 spsCancelled、未知金鑰映射成 spsUnsupported
Result := PostToSigningService(Req.KeyIdentifier, Req.AlgorithmOID,
Req.Input, Signature);
end,
3, // RetryLimit:總共至多四次嘗試
1048576, // MaxInputBytes
65536); // MaxSignatureBytes
Opts := HPDFCMSDefaultOptions(palBaseline_B_B);
Opts.SignatureScheme := cmsRSAPSS;
Opts.DigestAlgorithm := cmsdaSHA256;
Opts.PSSSaltLength := 32;
// contract-unsigned.pdf 儲存時帶著 AddSignedSignatureField 排好的佔位符
InStream := TFileStream.Create('contract-unsigned.pdf', fmOpenRead or fmShareDenyWrite);
OutStream := TFileStream.Create('contract-signed.pdf', fmCreate);
try
HPDFCMSSignPDFStreamWithProvider(InStream, OutStream, CertDER,
Provider, 'contracts-2026', Opts);
finally
OutStream.Free;
InStream.Free;
Provider.Free;
end;
end;
六種狀態,各自該觸發什麼
THPDFSignatureProviderStatus 有六個值,因為每個值的正確反應不同。spsInvalid 是密碼學失敗,該進安全日誌。spsUnsupported 表示提供者沒實作這個演算法,屬於部署問題。spsMalformed 表示請求或回應形狀不對。spsProviderError 是傳輸或設備故障,重試可能成功。spsCancelled 表示有人拒絕了權杖提示,重試只會惹人煩
遠端提供者把這套政策內建了。它只在 spsProviderError 上重試,每次嘗試前先查取消登錄簿,呼叫傳輸之前就把空輸入或超尺寸輸入擋成 spsMalformed,並把帶著空簽章或超尺寸簽章的「有效」答案降級為 spsMalformed。驗證這一側,提供者的答案映射到驗證狀態:spsInvalid 變 svSignatureInvalid,spsMalformed 變 svMalformed,spsProviderError 與 spsCancelled 都變 svCryptoProviderError,spsUnsupported 在平台提供者也拒了這個請求之後變 svUnsupportedAlgorithm
自己寫提供者的話,抵抗把每個意外狀況都映射成 spsInvalid 的衝動。那會把缺一個 DLL 或服務權杖過期,變成「客戶的簽章是偽造」的報告,而總得有人去向客戶解釋這句話。數學真的失敗時才回 spsInvalid。簽章方向,只有連同非空簽章才回 spsValid;提供者回報成功卻不帶位元組時,兩個入口點都會引發 EHPDFCMSError
提供者的簽章落在檔案裡的哪裡
兩個函式把提供者接到真實的 PDF 位元組。HPDFCMSBuildSignedDataWithProvider 從你在別處算好的文件摘要建 detached CMS;摘要長度必須符合 Options.DigestAlgorithm,SHA-384 設定期待 48 位元組,收到 32 位元組的 SHA-256 摘要會以例外拒絕。HPDFCMSSignPDFStreamWithProvider 簽的是 AddSignedSignatureField 排好的佔位符:雜湊兩個 /ByteRange 視窗、向提供者要對 signed attributes 的簽章,然後把十六進位結果補進 /Contents。CMS 長過佔位符時,這個呼叫引發例外,並在輸出串流裡還原未簽署的佔位符,而不是截斷任何東西
版面與 PFX 簽章路徑寫出的相同:兩個跳過十六進位簽章視窗的範圍。驗證器拿這個形狀要求每一份檔案,檢查兩個範圍之間的空隙是否恰好只有這張簽章的 /Contents 字串而無其他,否則回報 svInvalidByteRange。因此透過 PKCS#11 權杖簽出的文件,用的是與簽章驗證程式碼相同的驗證路徑,跟從 PFX 檔簽出的一樣。演算法選擇之上的設定檔規則見Delphi 中的 PAdES 基線簽章,PKCS#11 轉換對付的 ECDSA 編碼陷阱見ECDSA CMS 驗證與 P1363 簽章格式
推出後量子簽章,而不把讀者晾在半路
沒實作 ML-DSA 的 PDF 閱讀器會把這種簽章回報為無法驗證,看文件的人看到的是警告而不是綠色勾勾。這使遷移變成排程問題。讓 RSA 或 ECDSA 繼續當今天驗證器要評判的那張簽章。政策要求量子抗性證據之處,用 EnsureMLDSAExtensions 聲明延伸,並在古典簽章旁邊加上第二張由支援 ML-DSA 的簽署器產出的簽章。那張簽章來自你自己的 FIPS 204 簽署器——HotPDF 的 CMS 建構器不產生 ML-DSA 簽章,程式庫的角色是驗證它。等消費你文件的系統能驗證了,再把主簽章換過去
在這個計畫裡,HotPDF 蓋住你從 Delphi 掌控的兩端。它的 CMS 建構器透過上述任一提供者以 RSA PKCS#1 v1.5、RSA-PSS、ECDSA、Ed25519 與 Ed448 簽章;它的驗證器從一次呼叫檢查 ML-DSA、EdDSA 與古典演算法,並把實際找到的 OID 記進結果。HotPDF 是 Delphi 與 C++Builder 的原生 VCL PDF 元件,不需要外部 PDF 執行階段,簽章與驗證程式碼直接編譯進你的執行檔。版本、簽章功能集與試用下載,都在 HotPDF Delphi PDF component 頁面