签名前必须验证certificate.hasprivatekey为true,pfx加载需指定exportable和machinekeyset标志;itextsharp签名pdf须用filestream读源文件、避免覆盖原文件;时间戳须显式设为utc格式并嵌入完整证书链。

签名前必须验证证书私钥是否可用
很多开发者卡在签名失败,根本原因不是代码写错,而是 Certificate 对象没绑定可访问的私钥。Windows 上常见于 PFX 文件导入时勾选了“标记密钥为不可导出”,或证书存储在受保护的硬件模块中。
实操建议:
- 用
cert.HasPrivateKey明确检查,别只看证书是否存在 - 若为 PFX,加载时务必传入
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet,否则 .NET 可能静默丢弃私钥 - 在 Linux/macOS 上用
dotnet 6+运行时,需确认 OpenSSL 版本 ≥ 1.1.1,且 PFX 中私钥是 RSA(ECDSA 签名支持有限)
用 iTextSharp.LGPLv2.Core 签名 PDF 时的路径陷阱
iTextSharp.LGPLv2.Core 是目前 C# 中最易上手的免费 PDF 签名库,但它对输入输出路径极其敏感——它不接受流式读写混合,且要求源 PDF 必须是完整文件路径或 MemoryStream(不能是只读流)。
常见错误现象:ArgumentException: Document has no pages 或签名后 PDF 打不开。
实操建议:
- 源 PDF 一律用
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)加载,别用File.ReadAllBytes()后直接喂给PdfReader - 签名目标必须是新文件路径,或用
MemoryStream接收结果再写入磁盘;直接覆盖原文件会触发文件锁异常 - 若 PDF 含 AcroForm 表单字段,签名前先调用
stamper.FormFlattening = true,否则签名域可能被表单重绘破坏
签名时间戳必须显式设置,否则默认用本地系统时间
PDF 数字签名的时间戳(M/D/YYYY HH:mm:ss 格式)由代码生成,不是由证书自动注入。不设时间或设错格式,会导致 Adobe Reader 提示“签名时间无效”或“签名时间早于证书有效期”。
实操建议:
- 用
DateTime.Now.ToUniversalTime().ToString("MM/dd/yyyy HH:mm:ss"),别用ToString()默认格式(区域差异大) - 若需可信时间戳(TSA),必须接入第三方服务(如 Digistamp、GlobalSign TSA),iText 不内置 TSA 客户端;自行实现需处理 PKCS#7 时间戳响应解析
- 签名后用
pdfReader.GetPdfObject(pdfReader.Catalog.Get(PdfName.DOCMDP))检查签名字典是否含M字段,确认时间已写入
签名后 PDF 验证失败的三个高频原因
签名成功不等于验证通过。Adobe 或系统验证器报“签名损坏”“证书链不完整”“摘要不匹配”,往往和底层字节处理有关。
实操建议:
- 确保签名前 PDF 未被其他程序(如 Acrobat、预览)独占打开;Windows 下文件锁会导致签名数据写入不全
- 签名时禁用压缩:设置
stamper.Writer.SetPdfVersion(PdfWriter.PDF_VERSION_1_6)并调用stamper.Writer.SetFullCompression(false),避免增量更新与压缩冲突 - 证书链必须完整嵌入:用
new Org.BouncyCastle.X509.X509CertificateParser().ReadCertificates(certBytes)解析全部中间证书,传给MakeSignature.SignDetached的chain参数,不能只传叶子证书
真正麻烦的是证书链拼接顺序和 TSA 响应校验——这两步没有标准封装,出问题时日志里几乎不报具体哪一环断了。










