Delphi 版 PDFium Component 會對每一枚 ECDSA 與 EdDSA PDF 簽章套用 ISO/TS 32002 演算法設定檔,並把判定結果寫進 TPadesSignatureValidation.AlgorithmPolicyStatus。只有 P-256、P-384、P-521、三條 Brainpool r1 曲線、Ed25519 與 Ed448 能過關,而且摘要必須配對正確,配對錯了就舉發 ppeiSignatureAlgorithmMismatch。這套政策比聽起來更要緊。落在 brainpoolP160r1 上的簽章,或是拿 P-256 金鑰去簽 SHA-512 摘要,在數學層面都能驗得漂漂亮亮,於是 Windows CryptoAPI 說簽章值沒問題,嚴格的 PDF 2.0 驗證器卻把檔案打回票。政策檢查補的就是這道縫,而且它刻意與「簽章位元組在密碼學上正不正確」劃清界線
ISO/TS 32002 對橢圓曲線簽章到底允許哪些組合?
ISO/TS 32002 在 PDF 簽章裡恰好允許六條 ECDSA 曲線與兩種 EdDSA 方案,並把每條曲線跟它能搭配的摘要長度綁在一起。PDFium Component 把這張表編進 PadesCurveDigestAllowed,以簽署者憑證裡的曲線 OID 為鍵。NIST 曲線從嚴:摘要的位元寬度必須與曲線一致,SHA-2 或 SHA-3 皆可。Brainpool 曲線放得寬,接受自家寬度或任何更寬的:
- P-256(
1.2.840.10045.3.1.7):僅限 SHA-256 或 SHA3-256 - P-384(
1.3.132.0.34):僅限 SHA-384 或 SHA3-384 - P-521(
1.3.132.0.35):僅限 SHA-512 或 SHA3-512 - brainpoolP256r1(
1.3.36.3.3.2.8.1.1.7):256 到 512 位元的任何 SHA-2 或 SHA-3 摘要 - brainpoolP384r1(
1.3.36.3.3.2.8.1.1.11):384 或 512 位元的 SHA-2 / SHA-3 - brainpoolP512r1(
1.3.36.3.3.2.8.1.1.13):僅限 SHA-512 或 SHA3-512
EdDSA 沒有曲線可選,也沒有摘要可選,這正是它的規則只談編碼、不談強度的原因。按 RFC 8419,Ed25519 的 SignerInfo 必須宣告 SHA-512 作為 digestAlgorithm 且不帶參數;Ed448 的 SignerInfo 在 PAdES 永遠會走的 signed-attributes 路徑上,必須宣告 id-shake256-len(2.16.840.1.101.3.4.2.18)並附上恰好 512 的 INTEGER 參數。兩種方案的簽章 AlgorithmIdentifier 與憑證公鑰 AlgorithmIdentifier 都必須完全不帶參數。要是產生器在這裡塞了個 NULL——RSA 編碼器把很多 ASN.1 函式庫慣出來的壞習慣——做出來的簽章就不合規,儘管金鑰與簽章值本身都沒毛病
PDFium Component 怎麼從 CMS 抽出演算法三元組
PDFium 本身答不了這個問題:它的公開簽章 API 會讀簽章字典,卻既不驗 CMS,也不透露簽署者憑證用的是哪條曲線。所以建構在 PDFium 上的 PAdES 檢視層自己動手解析 CMS SignedData(RFC 5652)。InspectPadesSignatureAlgorithm 先讀第一個 SignerInfo 的 digestAlgorithm 與 signatureAlgorithm,接著找出簽署者憑證、讀它的 SubjectPublicKeyInfo,取得金鑰演算法與曲線。憑證查找刻意設了邊界:CMS certificates 集合裡最多檢查 64 張憑證,比對方式是拿 issuerAndSerialNumber 裡的簽發者與序號做逐位元組精確比對,只有集合裡恰好只有一張解析得動的憑證時,才退而求其次認定「就是這一張」。從無序集合裡撈第一張 EC 憑證當然省事,但那等於讓 CA 憑證替簽署者決定它用的是哪條曲線
uses
PDFium, FPdfPades;
const
StatusNames: array[TPadesCryptoStatus] of string =
('not checked', 'valid', 'invalid', 'unsupported', 'indeterminate');
var
Pdf: TPdf;
R: TPadesValidationResult;
A: TPadesSignatureAlgorithmInfo;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice-ecdsa-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
for I := 0 to Length(R.Signatures) - 1 do
begin
A := R.Signatures[I].AlgorithmInfo;
Writeln('Signature ', I);
Writeln(' digestAlgorithm : ', string(A.DigestAlgorithmOid));
Writeln(' signatureAlgorithm : ', string(A.SignatureAlgorithmOid));
Writeln(' public key / curve : ', string(A.PublicKeyAlgorithmOid),
' / ', string(A.CurveOid));
Writeln(' policy : ',
StatusNames[R.Signatures[I].AlgorithmPolicyStatus]);
end;
if ppeiSignatureAlgorithmMismatch in R.Issues then
Writeln('At least one signature violates the ISO/TS 32002 profile');
finally
Pdf.Free;
end;
end;
TPdf.ValidatePades 在合規檢查過程中套用這套政策,起點是它在 CMS 裡找到的憑證;TPdf.ValidatePadesTrust 則拿 Windows CryptoAPI 實際用來驗證的那張簽署者憑證重跑一遍,最後說了算的是 CryptoAPI 回報的憑證。所有原始輸入都落在 TPadesSignatureAlgorithmInfo 裡,包括 DigestParametersPresent、DigestParameterBits、SignatureParametersPresent 與 PublicKeyParametersAreNamedCurve,所以每筆拒絕都能從記錄裡解釋清楚,不必去翻日誌
P-256 簽章配 SHA3-256 為什麼過不了政策?
只要 CMS 的 digestAlgorithm 與 ECDSA signatureAlgorithm 暗示的摘要對不上,P-256 簽章就過不了 PDFium Component 的政策,就算兩者各自對這條曲線都算合格也一樣。EvaluatePadesSignatureAlgorithm 先把 ecdsa-with-SHA256、ecdsa-with-SHA3-256 與它們的兄弟映射成摘要,跟宣告的 digestAlgorithm 比對,查曲線表之前只要有一絲出入就回傳 pcsInvalid。這種情況真實存在:簽署工具把雜湊換成 SHA3-256,卻留著寫死的 ecdsa-with-SHA256 識別碼,做出來的檔案任何合規驗證器都無法一致地解讀。這個函式是公開的,所以可以用單元測試把整張矩陣釘死,不必真的造出 PDF:
var
Info: TPadesSignatureAlgorithmInfo;
begin
Info := Default(TPadesSignatureAlgorithmInfo);
Info.Family := psafEcdsa;
Info.PublicKeyAlgorithmOid := '1.2.840.10045.2.1'; // id-ecPublicKey
Info.PublicKeyParametersPresent := True;
Info.PublicKeyParametersAreNamedCurve := True;
Info.CurveOid := '1.2.840.10045.3.1.7'; // P-256
Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.8'; // SHA3-256
Info.SignatureAlgorithmOid := '2.16.840.1.101.3.4.3.10'; // ecdsa-with-SHA3-256
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsValid);
Info.SignatureAlgorithmOid := '1.2.840.10045.4.3.2'; // ecdsa-with-SHA256
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsInvalid); // 摘要不匹配
Info.CurveOid := '1.3.36.3.3.2.8.1.1.1'; // brainpoolP160r1
Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.1'; // SHA-256,此時長度一致
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // 曲線不在設定檔內
end;
曲線編碼吃同一套嚴格標準。RFC 5480 §2.1.1 允許 ECParameters 是具名曲線 OID、隱含曲線(NULL)或整套顯式參數,而 PKIX 設定檔要求具名形式。當 id-ecPublicKey 憑證不帶參數、帶隱含參數或帶顯式參數時,PDFium Component 一律回傳 pcsInvalid,因為顯式參數讓攻擊者得以描述一條長得像標準曲線的冒牌貨。至於具名正確、只是不在 ISO/TS 32002 清單上的曲線,例如前面的 brainpoolP160r1 或 secp256k1,則回傳 pcsUnsupported
無效、不支援還是不確定:誠實解讀狀態值
AlgorithmPolicyStatus 的三個非有效狀態各有各的意思,把它們一股腦倒進「失敗」這個桶子,等於把稽核人員需要的資訊倒掉。pcsInvalid 表示可辨識的演算法組合格式錯誤或配對失當;它會把 ppeiSignatureAlgorithmMismatch 加進 TPadesValidationResult.Issues,並把整體的 IntegrityStatus 壓到 pcsInvalid,所以就算 CMS 簽章值本身驗得過,IsCryptographicallyValid 也會回傳 False。pcsUnsupported 表示曲線或摘要不在設定檔點名之列,這是能力範圍的結論,不是遭竄改的證據。pcsIndeterminate 表示釘不住簽署者憑證,通常是 CertificateSet 裡候選有好幾張、又沒有精確的 issuerAndSerialNumber 比對結果,程式於是拒絕猜曲線;v3.124.0 起它也標記以 SHA-1 或 SHA-224 這類 112 位元摘要簽出的 RSA 簽章——這些已不再是當前驗證公認的選擇。同樣的區分也適用於 CryptoAPI 驗不了 Ed25519 或 Ed448 的機器上的 EdDSA:CmsSignatureStatus 停在 pcsUnsupported,而 AlgorithmPolicyStatus 照樣可以是 pcsValid,因為編碼正確,缺的只是驗證器。如果您正在追 Adobe 或 DSS 系驗證器給的拒絕原因,驗證器為何打回 PAdES 簽章指南涵蓋了其他常見成因
var
Pdf: TPdf;
Options: TPadesTrustValidationOptions;
R: TPadesValidationResult;
S: TPadesSignatureValidation;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice-ecdsa-signed.pdf';
Pdf.Active := True;
Options := TPadesTrustValidationOptions.Default; // 離線,不查廢止
R := Pdf.ValidatePadesTrust(Options);
for I := 0 to Length(R.Signatures) - 1 do
begin
S := R.Signatures[I];
if S.IsDocumentTimeStamp then
Continue;
case S.AlgorithmPolicyStatus of
pcsInvalid: Writeln(I, ': reject, algorithm combination is invalid');
pcsUnsupported: Writeln(I, ': manual review, curve or digest outside the profile');
pcsIndeterminate: Writeln(I, ': manual review, signer not identified or digest no longer agreed');
pcsValid:
if S.AlgorithmInfo.Family = psafRsa then
Writeln(I, ': RSA digest suite accepted, check key size yourself')
else
Writeln(I, ': EC/EdDSA profile satisfied');
end;
end;
Writeln('Cryptographically valid: ', R.IsCryptographicallyValid);
finally
Pdf.Free;
end;
end;
pcsValid 不保證什麼?
AlgorithmPolicyStatus = pcsValid 只擔保一件事:ECDSA 或 EdDSA 簽章用了核准的曲線、摘要長度一致且編碼正確,RSA 簽章用了當前套件認可的摘要;簽章值對不對,它一個字都不說。v3.124.0 之前,EvaluatePadesSignatureAlgorithm 的 RSA 分支刻意放得很寬:PKCS #1 弧 1.2.840.113549.1.1.* 底下任何 signatureAlgorithm 都回傳 pcsValid,老古董 sha1WithRSAEncryption 也不例外。PDFiumPas v3.124.0 起,RSA 分支改套 ETSI TS 119 312 簽章套件。MD2、MD4、MD5 摘要是 pcsInvalid。SHA-1 與 SHA-224 這類 112 位元摘要是 pcsIndeterminate,所以 SHA-1 簽章保留整體完整性結果、被標記待人工複核,而不是直接拒收。digestAlgorithm 與簽章演算法固定的摘要不一致——例如拿 sha256WithRSAEncryption 去簽 SHA-1 摘要——或在非 RSA 簽署者金鑰上用 RSA 簽章演算法,都是 pcsInvalid,以 ppeiSignatureAlgorithmMismatch 回報並判定完整性失敗。找不到簽署者憑證給 pcsIndeterminate,與 ECDSA 情形相同;認不得的摘要或非簽章用途的 RSA OID 則給 pcsUnsupported。模數長度照舊不檢查,PSS 參數也不在這裡驗(RFC 4055 RSASSA-PSS-params 一文說明簽署端如何編碼這些參數),SHA-1 或 MD5 摘要另外還會觸發獨立的 ppeiBadDigestAlgorithm 問題。同樣地,簽章數學、憑證鏈與廢止狀態仍是 CmsSignatureStatus、CertificateTrustStatus 與 RevocationStatus 的職責,它們來自 Windows CryptoAPI。請把 pcsValid 當成「演算法設定檔成立」,千萬別當成「這把金鑰夠強」
對會收簽署版 PDF 發票、合約或封存包裹的 Delphi 應用程式來說,實用配置很簡短:跑 ValidatePadesTrust,遇到 ppeiSignatureAlgorithmMismatch 就拒收,pcsUnsupported 與 pcsIndeterminate 轉給人工,RSA 金鑰長度下限自己訂,因為政策不會替您把關。PDFium Component for Delphi and Lazarus 隨附 PAdES 驗證器、證據報告產生器與簽署管線,同一套函式庫既能產生這些簽章,也能端到端把它們驗過