為了使已簽署的 PDF 在多年後仍能通過驗證(在簽署憑證過期且其 CA 輪替之後),PDFium 元件可以在 Windows 上產生長期 PAdES 簽章:用於可信時間的 RFC 3161 時間戳記、嵌入驗證資料的文件安全性存放區(DSS),以及覆蓋整個檔案的封存 DocTimeStamp(文件時間戳記)。這三項新增內容即為 PAdES B-T、B-LT 和 B-LTA 基準等級,且 PDFium 元件能透過單次簽署呼叫達到所有這些等級。這是一項僅限 Windows 的功能,受 PadesCryptoAvailable 限制
本文是本系列的第三篇。第一篇 在 Delphi 中使用 PDFium VCL 進行 PAdES B-B 簽署 介紹了基本簽章;第二篇 在 Delphi 中檢查 PDF 簽章與 PAdES 等級 介紹了讀回簽章並判斷其達到的等級。本文關注的重點是封存:簽章必須在憑證過期後依然存活,且在以年(而非週)計的保留期內保持可驗證狀態
為什麼有效的 PDF 簽章隨時間推移會失效?
基本簽章僅回答了一個問題:這些位元組在簽署後是否被修改過。它本身無法證明簽署發生的時間,而這個漏洞正是它日後失效的原因。CMS 結構攜帶了簽署時間屬性,但簽署者是根據自己的時鐘寫入該數值的,因此驗證器沒有理由信任它。當簽署憑證隨後過期或憑證授權機構(CA)撤銷它時,嚴格的驗證器將無法再區分在憑證有效期間進行的簽章,與憑證失效後偽造的簽章。簽章在建立當天是沒有問題的,但由於非自身原因,日後會變得無法驗證
長期封存透過兩個步驟解決了這個問題。首先,受信任的協力廠商,即時間戳記授權機構(TSA),使用密碼學證明該時間,使其不再依賴簽署者的誠實度。其次,建立和檢查信任鏈所需的每個憑證、撤銷回應和 CRL 都嵌入在 PDF 內部,因此驗證不依賴於當有人查看時可能已經消失的伺服器。PAdES(標準化為 ETSI EN 319 142-1)將這些分層為基準等級,且 PDFium 元件會寫入每個等級所需的結構
什麼是 LTV 或帶有封存時間戳記的 PDF 簽章?
PAdES 定義了四個相互構建的基準等級,且 PDFium 元件透過 TPadesLevel 列舉 (plB_B, plB_T, plB_LT, plB_LTA) 將它們公開。B-B 是普通的簽章。B-T 在簽章值上加入受信任的時間戳記。B-LT(長期)加入了嵌入的驗證資料,這正是大多數人所指的「LTV」屬性。B-LTA(長期且帶有封存時間戳記)將所有內容包裝在另一個覆蓋整個檔案的時間戳記中,因此可以證明嵌入的資料本身在封存之初就是完好無損的
實際的理解是一個階梯。B-T 證明了「何時」。B-LT 證明了「使用什麼」仍能檢查該簽章。B-LTA 證明了整個封套自封裝後未被篡改,這是您需要續約的等級,即在舊的文件時間戳記(DocTimeStamp)過期之前加入新的時間戳記,以無限期延長封存期限。對於具有長期保留義務的合約或合規記錄, B-LTA 是目標;B-LT 則是務實的底線
加入受信任時間:搭配 RFC 3161 的 PAdES B-T
PDFium 元件透過在 TPadesSignOptions 上設定兩個欄位將 B-B 簽章轉換為 B-T:將 Level 提升至 plB_T 並提供 TsaUrl。當兩者都存在時,簽署管線會使用 SHA-256 雜湊原始簽章八位元組(octets)、將該摘要封裝為 RFC 3161 messageImprint、透過 WinHttp 將請求 POST 傳送至 TSA,並依照 EN 319 142-1 §6.3 的規定,將傳回的 Token 作為 signature-time-stamp 未簽署屬性(OID 1.2.840.113549.1.9.16.2.14)嵌入至 SignerInfo unsignedAttrs [1] 中。該時間戳記特別覆蓋了簽章值本身,這將受信任時間繫結至該精確簽章
uses
PDFium, FPdfPades;
procedure SignWithTimestamp;
var
Pdf: TPdf;
Options: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
if not Pdf.PadesCryptoAvailable then
raise Exception.Create('PAdES 簽署後端僅限 Windows');
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F6071829304152637485960A1B2C3D';
Options.Level := plB_T;
Options.TsaUrl := 'http://timestamp.example-tsa.com/tsr';
Options.Reason := 'Contract execution';
// Nonce 保持為 0:元件會衍生唯一的防重放值。
Pdf.SignPades('contract-bt.pdf', Options);
finally
Pdf.Free;
end;
end;
CertificateThumbprint 是「目前使用者」的「個人」(MY)憑證存放區中您可以使用的憑證私鑰的 SHA-1 雜湊十六進位值。Nonce 欄位是 RFC 3161 的防重放(anti-replay)隨機數;將其保持為零,元件將根據計數器、時鐘、Tick 計數和程序 ID 的 SHA-256 摺疊,為每個請求衍生一個唯一的隨機數,因此在同一毫秒發出的兩個請求不會發生碰撞。只有在您有更信任的密碼學隨機數產生器(RNG)時,才傳遞您自己的數值。一個實際的局限:B-T 需要可連線的 TSA,因此簽署時現在會發起網路呼叫,並承受 TSA 的可用性與延遲(latency)
嵌入驗證資料:一次呼叫完成 B-LT 與 B-LTA
升級至長期驗證只需單一變更:將 Level 設定為 plB_LTA 並保留 TsaUrl。接著 PDFium 元件會在單次 SignPades 呼叫中執行完整的階梯。它簽署 B-B、為 B-T 加入時間戳記、為 B-LT 寫入文件安全性存放區(DSS),並為 B-LTA 附加封存時間戳記,每個步驟都是獨立的增量更新(incremental update),使先前的位元組在位元組級保持完整
procedure SignForArchive;
var
Pdf: TPdf;
Options: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F6071829304152637485960A1B2C3D';
Options.Level := plB_LTA; // 驅動 B-B -> B-T -> B-LT -> B-LTA
Options.TsaUrl := 'http://timestamp.example-tsa.com/tsr';
Options.Location := 'Head Office';
// 一次呼叫即寫入時間戳記、DSS 和封存 DocTimeStamp。
Pdf.SignPades('contract-lta.pdf', Options);
finally
Pdf.Free;
end;
end;
對於 B-LT 階段,PDFium 元件使用 CertGetCertificateChain 建立憑證鏈,並寫入 /DSS 字典,其中在 /Certs 陣列中攜帶簽署者憑證以及每個中間 CA(包含平行的 /OCSPs 和 /CRLs 陣列),同時在等級 1 包含 /Extensions /ESIC 標記,完全符合 EN 319 142-1 §5.4.2 的規定。自我簽署的根憑證被刻意省略,因為 RFC 5652 §10.2.3 允許這樣做,且根憑證已是驗證器持有的信任起點(trust anchor)。其結果是一個讀者僅需手頭的檔案即可驗證的簽章
對於 B-LTA,該元件會附加一個文件時間戳記:一個帶有 /SubFilter /ETSI.RFC3161 的簽章字典,其 /ByteRange 覆蓋整個檔案,其 /Contents 保存了覆蓋該範圍的 RFC 3161 Token,並將 ESIC 標記提升至等級 2。這是 §5.4.3 和 TS 119 142-3 規定的封存步驟。因為文件時間戳記同時覆蓋了 DSS 和簽章,它證明了嵌入的驗證資料在封存時已存在且未被篡改,這正是長期保留原則必須能夠證明的
增強已存在的簽章
有時,簽章已經存在於文件上,您只需要加入或重新整理驗證存放區,例如為了封存目的,將協力廠商的 B-B 簽章提升至 B-LT。PDFium 元件透過 SaveAsPadesDss 直接公開了 DSS 寫入器,這會將 /DSS 和 /Extensions 結構作為增量更新附加至檔案末尾,而不碰觸現有的簽章位元組
procedure AddValidationStore;
var
Pdf: TPdf;
DssOpts: TPadesDssOptions;
Cert: TPadesDssMaterial;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'already-signed.pdf';
Pdf.Active := True;
DssOpts := TPadesDssOptions.Default;
DssOpts.EsicExtensionLevel := 1; // 1 代表 B-LT,2 代表 B-LTA
Cert.DerBytes := LoadSignerCertDer; // 您的 DER 編碼憑證
SetLength(DssOpts.Certs, 1);
DssOpts.Certs[0] := Cert;
Pdf.SaveAsPadesDss('already-signed-lt.pdf', DssOpts);
finally
Pdf.Free;
end;
end;
您提供驗證器所需的 DER 編碼憑證,以及選用的 OCSP 回應和 CRL。將 EsicExtensionLevel 設定為 1(針對 B-LT 存放區)或設定為 2(在後續會有 DocTimeStamp 時)。將 IncludeVri 保持為其預設值 False:EN 319 142-1 §5.4.2.3 將每個簽章的 /VRI 字典視為選用項目,並建議不要使用它,除非您針對的特定閱讀器有此要求
值得規劃的代價與界限
三個實際的限制決定了這些等級的適用範圍。第一,密碼學後端僅限 Windows:簽署和時間戳記功能使用的是 Windows 平台 CNG 和 WinHttp 存放區,因此在其他平台上 PadesCryptoAvailable 會傳回 False,且 SignPades 會引發錯誤。在隨附文章中說明的讀取端檢查保持跨平台;僅簽章寫入功能與 Windows 繫結。第二,B-T 和 B-LTA 依賴於時間戳記授權機構(TSA),這意味著在簽署時會發起網路往返,且外部服務的運作時間和頻率限制會成為您簽署路徑的一部分
第三,每個等級都需要儲存代價。每個階段都會附加增量更新,而非重寫檔案,DSS 嵌入了完整的憑證鏈,且封存 DocTimeStamp 為其 Token 預留了空間,因此 B-LTA 檔案明顯大於 B-B 原始檔案。這是一個刻意的權衡:您現在花費位元組空間,以換取未來的可驗證性。對於大多數合約和合規封存來說,這是正確的權衡,但值得在您自己的文件上進行測量,而非盲目假設。當您需要確認產生的檔案是否確實達到了預期的等級時,請使用 在 Delphi 中檢查 PDF 簽章與 PAdES 等級 中介紹的技巧進行驗證;如果您要更廣泛地增強文件管線,稽核 PDF 安全風險 包含相關檢查的介紹
此處介紹的 PAdES 簽署、DSS 和封存時間戳記功能隨附於適用於 Delphi 和 C++Builder 的 PDFium 元件 中,其產品頁面提供了完整的簽署參考手冊以及平台需求說明