判定一份 PDF 簽章在 eIDAS 下是否合格,意味著回答一個與密碼學無關的問題:簽章作成之時,簽發憑證的信任服務是否被某個會員國列為合格。答案在一個信任清單裡——按轄區發布的 XML 文件——而那份文件的全部價值取決於其真實性。所以 PDFium 元件在有人為它背書之前,拒絕查看其中內容。TPdfEuropeanTrustedList.ParseAuthenticated 在解析任何一個服務之前,把完整的原始位元組交給呼叫端提供的 IPdfTrustedListAuthenticator,而且只有該驗證器明確通過,才建立快照
這個順序就是設計。這個功能裡其他一切,包括那些看起來不便的部分,都由它推出
解析成功不等於可信
一份解析乾淨的信任清單,告訴您 XML 格式正確。它沒有告訴您是誰寫的。既然整個合格狀態判定都壓在清單上,因為解析通過就接受一份清單,會讓判定失去意義:能替換清單的攻擊者,可以宣稱自己的憑證授權單位合格
同樣的推理適用於快取,而這正是值得點名的陷阱。快照快取把原始 XML 與 SHA-256 摘要一起儲存,載入時把摘要相符當成清單真實的證明,是很自然的想法。但它不是。由儲存檔案的同一個行程、在不涉及任何金鑰的情況下算出的摘要,只驗證位元組自您寫入後沒有改變;如果清單在快取時就是偽造的,摘要只確認它還是同一份偽造清單。所以載入快取快照,要走與解析新清單相同的驗證器。完整性與真實性是兩種不同性質,只有其中一種需要金鑰
uses
FPdfTrustedList;
type
TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
public
function Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
end;
function TListAuthenticator.Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
begin
// 您的政策放這裡:對照您以帶外方式釘選的清單簽署憑證
// 驗證 XMLDSIG 封裝簽章,並為稽核軌跡描述您檢查了什麼
Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
if Result then
AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;
var
List: TPdfEuropeanTrustedList;
Cache: TFileStream;
begin
List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
TListAuthenticator.Create, TPdfTrustedListOptions.Default);
// 快照存在,正是因為驗證器說了同意
Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
try
List.SaveCache(Cache);
finally
Cache.Free;
end;
end;
驗證器不擁有網路政策
PAdES 驗證器沒有立場決定怎麼連上信任清單的清單、要不要走代理、重試多久一次、或某個轄區連不上時該怎麼辦。那些是應用程式與部署的決策,在受監管環境裡還經常被稽核。所以更新經由 IPdfTrustedListSource 到達,它收到一個 URI 與位元組上限,回傳位元組
元件確實執行的,是讓更新成為更新而不是替換的那些不變式。Update 要求轄區不變、序號嚴格遞增、簽發時間不倒退。這三項檢查擊退最明顯的降級攻擊:重播一份仍列出已撤下服務的舊清單,或換入另一個轄區、其服務您從未打算信任的清單
剖析器上限,以及完全不接受 DTD
TPdfTrustedListOptions 為 XML 大小、記號數、巢狀深度、服務數、憑證數與單張憑證大小設上限,並以 Default 類別函式提供可用值。信任清單是尺寸可預期的公開文件,所以設界限很便宜,而且沒有任何合法清單需要超過它們
另外,無條件地,剖析器拒絕 DTD 與實體宣告。這一個拒絕同時關閉實體擴張阻斷服務與外部實體洩漏兩條路,而且毫無代價,因為信任清單不用實體。任何可從不受信任輸入觸及的 XML 剖析器都應該這樣設定;這裡的差別是拒絕不可設定,所以它不會被一個好心的選項改動關掉
合格狀態記在憑證鏈信任旁邊,而不是併入其中
評估側刻意分開。TPadesTrustValidationOptions.QualifiedTrustEvaluator 接受一個 IPdfQualifiedTrustEvaluator,信任清單快照實作了它。驗證期間,評估器收到葉憑證、憑證鏈與一個驗證時間,以精確 DER 比對把服務憑證與簽署者及憑證鏈比對,把服務狀態、服務型別識別碼與該時點的限定詞 URI 組合起來,回傳一筆評估記錄
結果落在每個簽章的兩處:QualifiedTrustStatus 是粗粒度狀態,QualifiedTrust 是帶轄區、提供者名稱、服務名稱、型別識別碼、狀態與狀態起始時間的完整評估。它不做的,是改動 CertificateTrustStatus。系統憑證鏈信任與合格狀態回答的是不同問題,一份把兩者合併的報告無法區分「受信任但不合格」與「合格但憑證鏈驗證不過」——兩者都真實存在,而且需要不同處理
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
I: Integer;
begin
Options := TPadesTrustValidationOptions.Default;
Options.CheckRevocation := True;
Options.QualifiedTrustEvaluator := List; // 經驗證的快照
Options.QualifiedValidationTime := SigningTime; // 而不是 Now
Report := Pdf.ValidatePadesTrust(Options);
for I := 0 to High(Report.Signatures) do
if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
Writeln(Format('signature %d qualified by %s / %s (%s)',
[I, Report.Signatures[I].QualifiedTrust.Territory,
Report.Signatures[I].QualifiedTrust.ProviderName,
Report.Signatures[I].QualifiedTrust.ServiceName]))
else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
// 沒有符合的服務,或快照無法為這個時間作答
Writeln(Format('signature %d: qualified status undetermined', [I]));
end;
為什麼驗證時間不是現在
因為合格性是某個時刻的性質。信任服務可以被授予合格狀態,之後被撤下,再之後恢復,每一次轉換在清單裡都帶著一個起始時間。在服務合格期間作成的簽章,之後保持合格;在授予之前作成的簽章,不會追溯變成合格。所以拿目前時間去評估,兩個方向都會給錯答案
清單為此攜帶了所需的東西:每筆服務記錄有一個狀態起始時間,以及一個區分歷史項與現行項的旗標,評估器對照您提供的時間組合它們。實務上那個時間來自簽章上的受信任時間戳記,而不是 CMS 裡宣稱的簽署時間,這正是長期驗證材料即使對一個看似政策查詢的問題也很要緊的原因;時間戳記與 DSS 那一側在長期簽章文章中涵蓋
您仍然要自己建什麼
三件事,而且沒有一件屬於 PDF 程式庫。驗證器,意思是對照您透過信任管道取得的清單簽署憑證做真正的 XMLDSIG 驗證。取回政策,意思是多久、怎麼刷新,以及刷新失敗時應用程式怎麼辦。還有轄區範圍,意思是您到底攜帶哪些清單——這是關於您的交易對手在哪些會員國簽署的業務決策
您從元件得到的是容易錯得微妙的那部分:先驗證後解析的順序、有界限且無實體的 XML 解析、單調更新不變式、精確 DER 服務比對、歷史狀態評估,以及與一般憑證鏈信任保持分離的結果。如果您眼前的問題更基本——驗證器拒收了您認為沒問題的簽章——常見成因整理在為什麼驗證器拒收 PAdES 簽章,簽章檢視介面描述於檢視簽章與 PAdES 層級。元件能力列於 PDFium Delphi component 產品頁