教程:在 Delphi 中进行 PAdES PDF 签名
完整演练,将未签名的发票 PDF 转换为带有嵌入式 RFC 3161 时间戳的 PAdES-B-T 文档——这是 eIDAS、AdES 验证器和大多数公共部门门户所要求的级别。您将配置密钥提供程序(PKCS#11 智能卡或 Windows 证书存储)、选择合适的 PAdES 配置文件,并端到端验证签名文件。
完整演练,将未签名的发票 PDF 转换为带有嵌入式 RFC 3161 时间戳的 PAdES-B-T 文档——这是 eIDAS、AdES 验证器和大多数公共部门门户所要求的级别。您将配置密钥提供程序(PKCS#11 智能卡或 Windows 证书存储)、选择合适的 PAdES 配置文件,并端到端验证签名文件。
一个 VCL Forms 应用程序,加载 PDF、使用智能卡或 PFX 进行签名、嵌入可信时间戳,并将可验证的 PAdES 文件写入磁盘。
PAdES-B-B(基础)、PAdES-B-T(带时间戳)、PAdES-B-LT(带吊销数据的长期)或 PAdES-B-LTA(带归档时间戳的长期)。对于发票、合同和大多数公共部门工作流,PAdES-B-T 是合理的默认选项。
sgcSign 支持 10 种密钥提供程序。本教程涵盖用于硬件令牌的 TsgcPKCS11KeyProvider 以及用于已安装在 Windows 中的证书的 TsgcWindowsStoreKeyProvider。相同的代码路径适用于 PFX、PEM、Azure Trusted Signing、AWS KMS 和 Google KMS。
TsgcSignPDF 驱动签名流水线,TsgcSignProfile_PAdES 配置配置文件,TsgcSignatureVerifier 验证最终文档。每个步骤都暴露事件,让您可以在每个阶段检查发生了什么。
每个密钥提供程序位于自己的单元中。仅引用您需要的提供程序,避免链接不会使用的单元。
uses 子句本教程引入两个密钥提供程序(PKCS#11 和 Windows 存储)以及 PAdES 签名单元。sgcSign_TSA 是 RFC 3161 时间戳客户端——当您设置 TSA URL 时,PAdES 配置文件会自动使用它。
如果您只有 PFX 文件,请将 sgcSign_KeyProvider_PKCS11 替换为 sgcSign_KeyProvider_PFX,并按 5 分钟快速入门中所示使用 TsgcPFXKeyProvider。
uses Classes, SysUtils, // sgc sgcSign_KeyProvider_PKCS11, sgcSign_KeyProvider_WindowsStore, sgcSign_PDF, sgcSign_Profile_PAdES, sgcSign_TSA, sgcSign_Verifier;
两种常见选择——PKCS#11 硬件令牌(智能卡、USB HSM)或已位于 Windows 证书存储中的证书。
将 ModulePath 指向供应商的 PKCS#11 DLL(例如 SafeNet 的 C:\Windows\System32\eTPKCS11.dll)。Slot 属性选择令牌槽——使用 0 表示第一个可用令牌。PIN 通过 OnPinPrompt 事件传递,因此永远不会存储在源代码中。
调用 LoadFromToken 会打开会话,根据标签或指纹查找签名证书,并准备私钥句柄。没有任何字节离开令牌——实际的 RSA-PSS 或 ECDSA 签名都在设备上进行。
var vKeyProvider: TsgcPKCS11KeyProvider; begin vKeyProvider := TsgcPKCS11KeyProvider.Create(nil); try vKeyProvider.ModulePath := 'C:\Windows\System32\eTPKCS11.dll'; vKeyProvider.Slot := 0; vKeyProvider.CertificateLabel := 'My Signing Cert'; vKeyProvider.OnPinPrompt := procedure(Sender: TObject; var aPIN: string) begin aPIN := InputBox('Smart card', 'PIN:', ''); end; vKeyProvider.LoadFromToken; // vKeyProvider is now ready to sign finally // keep alive until signing finishes, then Free end; end;
如果您的证书已安装在 Windows 存储中——例如因为您使用带供应商微型驱动程序的合格 eID 卡——请改用 TsgcWindowsStoreKeyProvider。该提供程序与 CNG 通信,因此无论原始 CSP 是什么,SHA-256/SHA-384 都可以正常工作。
按主题、指纹或序列号匹配。将 StoreLocation 设置为 slCurrentUser 使用个人存储;slLocalMachine 使用计算机存储,通常需要管理员权限。
var vKeyProvider: TsgcWindowsStoreKeyProvider; begin vKeyProvider := TsgcWindowsStoreKeyProvider.Create(nil); vKeyProvider.StoreLocation := slCurrentUser; vKeyProvider.StoreName := 'MY'; vKeyProvider.Thumbprint := '1234ABCD5678EF901234567890ABCDEF12345678'; vKeyProvider.LoadFromStore; end;
PAdES-B-T 为签名添加 RFC 3161 时间戳,验证者可以据此证明文档是在已知时间点之前签署的——即使签名证书后来过期或被吊销也是如此。
Level 选择 AdES 级别——palB、palT、palLT 或 palLTA。对于 PAdES-B-T,将其设置为 palT;此时配置文件会要求一个非空的 TSA.URL。
HashAlgorithm 默认为 shaSHA256,这是每个现代验证者所期望的。SHA-384 和 SHA-512 也可用;故意不提供 SHA-1,因为 eIDAS 不再接受它。
SignatureField 子对象控制可见签名小部件在 PDF 内的放置位置——页码、矩形坐标以及可选的原因 / 位置 / 联系文本字段。对于隐形签名(默认),将其保留为空。
var vProfile: TsgcSignProfile_PAdES; begin vProfile := TsgcSignProfile_PAdES.Create(nil); vProfile.Level := palT; vProfile.HashAlgorithm := shaSHA256; // RFC 3161 timestamp authority vProfile.TSA.URL := 'http://timestamp.digicert.com'; vProfile.TSA.HashAlgorithm := shaSHA256; // Optional visible signature widget vProfile.SignatureField.Visible := True; vProfile.SignatureField.Page := 1; vProfile.SignatureField.Rect := Rect(50, 50, 250, 120); vProfile.SignatureField.Reason := 'Approved'; vProfile.SignatureField.Location := 'Madrid'; end;
将密钥提供程序和配置文件接入 TsgcSignPDF,然后使用输入和输出文件名调用 SignFile。
SignFile 读取输入 PDF、计算文档摘要、调用密钥提供程序生成签名、从 TSA 获取时间戳并写入结果。如果输入已包含签名,sgcSign 会附加增量更新——原始签名保持有效。
OnProgress 事件报告每个阶段(读取、哈希、签名、加盖时间戳、写入),对多 MB 文件的进度条很有用。
var vSigner: TsgcSignPDF; begin vSigner := TsgcSignPDF.Create(nil); try vSigner.KeyProvider := vKeyProvider; vSigner.Profile := vProfile; vSigner.OnProgress := procedure(Sender: TObject; const aPhase: string; aPct: Integer) begin Memo1.Lines.Add(Format('%s — %d%%', [aPhase, aPct])); end; vSigner.SignFile('invoice.pdf', 'invoice-signed.pdf'); finally vSigner.Free; end; end;
只有可验证的签名文档才有用。TsgcSignatureVerifier 一次调用即可验证加密签名、证书链和嵌入的时间戳。
VerifyFile 方法返回一个带有逐签名细分的 TsgcSignatureReport。对于每个条目,您将获得 Status(svValid、svInvalid、svUnknown)、签署者主题、实际检测到的 AdES 级别、时间戳值,以及任何链或吊销问题。
将 Report.Level 与期望级别进行比较——您要求的是 palT,因此任何报告为 palB 的验证都意味着时间戳请求悄无声息地失败了。TSA URL 通常是罪魁祸首。
var vVerifier: TsgcSignatureVerifier; vReport: TsgcSignatureReport; i: Integer; begin vVerifier := TsgcSignatureVerifier.Create(nil); try vReport := vVerifier.VerifyFile('invoice-signed.pdf'); for i := 0 to vReport.SignatureCount - 1 do begin Memo1.Lines.Add('Signer: ' + vReport.Signatures[i].Subject); Memo1.Lines.Add('Level: ' + vReport.Signatures[i].LevelAsString); Memo1.Lines.Add('Status: ' + vReport.Signatures[i].StatusAsString); Memo1.Lines.Add('TSA time: ' + DateTimeToStr(vReport.Signatures[i].TimestampUTC)); end; finally vVerifier.Free; end; end;
开发者首次签署 PAdES 文档时最常见的问题简短列表。
某些 TSA 仅支持 HTTPS,会以软错误的方式拒绝纯 http:// URL。如果您请求 palT 但 Report.Level 返回 palB,请将 TSA URL 切换到其 https:// 等效项。大多数公共 TSA 同时发布两种方案。
测试证书通常缺少 id-kp-documentSigning EKU。即使加密签名有效,Adobe Reader 也会显示警告。对于生产环境,请向 CA 申请带有明确文档签名 EKU 的证书。
PDF/A-2 和 PDF/A-3 归档要求所有吊销信息嵌入文档本身。请使用 palLT 或 palLTA——sgcSign 将在签名时收集 OCSP 响应和 CRL 并将其嵌入 DSS 字典。
某些 PKCS#11 驱动程序会在几秒钟不活动后关闭会话。在整个签名操作期间保持 TsgcPKCS11KeyProvider 实例处于活动状态,仅在 SignFile 返回后调用 Free。