大多数开发者都曾使用过带签名的PDF文件——比如带签名的合同、经过认证的银行对账单、政府表格。但“签名”这个词对不同的人意味着不同的东西。签名框中的手写痕迹和嵌入文件中的加密签名是两种完全不同的事物。只有其中一种能真正提供保障。
本指南将介绍PDF数字签名的工作原理、不同有效性状态的含义,以及如何编程验证这些签名。
你看到的签名 vs. 真正重要的签名
当有人使用绘图工具或图像印章对PDF文件进行“签名”时,他们实际上只是在页面上放置了一个签名图片。看起来正式,但没有任何安全保证。任何人都可以将该图片复制到其他文档中。
数字签名则不同。它是一种嵌入PDF结构中的加密凭证——与任何视觉元素都无关。当文档被数字签名时:
- 会计算文档内容的哈希值。
- 然后使用签名者的私钥对这个哈希值进行加密。
- 加密后的哈希值(即签名)将被存储在PDF中,并与签名者的证书链一同保存。
在验证签名时,你将使用证书中的公钥对签名进行解密,重新计算当前文档内容的哈希值,并进行比对。如果两者一致,说明文档自签名以来未被修改。如果不一致,或者证书不被信任,签名则无效。
四种签名有效性状态
并非所有的数字签名都相同。当你检查PDF签名时,可能会遇到以下四种状态之一:
| 签名状态 | 含义 | 应采取的措施 |
|---|---|---|
| 有效 | 哈希值匹配,证书链可信,且证书在签名时处于有效状态 | 信任该签名——将签名者身份与预期证书进行比对 |
| 无效 | 文档内容在签名后被修改,或签名数据损坏 | 拒绝该文档;它已被篡改或格式不正确 |
| 未知 | 签名结构完整,但无法验证证书(例如根证书不受信任、缺少OCSP等) | 无法信任该签名——要求重新签名或获取可信根证书 |
| 已撤销 | 证书在签发时有效,但之后被证书颁发机构(CA)撤销(例如密钥泄露) | 除非有LTV数据证明证书在撤销前是有效的,否则应拒绝该签名 |
“未知”状态常常让大多数开发者感到困惑。一个结构正确但使用自签名或企业内部证书的签名,在大多数工具中都会显示为“未知”,因为这些工具无法验证签发者。在内部文档工作流中,你可以明确信任该根证书;而对于外部方提供的文档,“未知”状态是不可接受的。
长期验证(LTV):为何时间戳很重要
证书会过期。如果某人五年前签署了一份文件,而其证书已经过期,那么该签名是否仍然有效?
这取决于是否启用了长期验证(LTV)。当LTV被嵌入时,PDF中将包含:
- 来自时间戳权威机构(TSA)的可信时间戳
- 确认签名时证书状态的OCSP响应或CRL数据
启用LTV后,你可以证明签名时证书是有效的,即使证书已过期。若没有LTV,则只能验证签名与当前证书状态,一旦证书过期或OCSP响应器被停用,这种验证将变得不可能。
对于需要长期保存(许多司法管辖区要求保存7年以上)的合同或监管文件,LTV并非可有可无。在构建签名验证流程时,必须始终检查是否存在LTV。
程序化验证PDF签名
使用Python配合pypdf库
这 pypdf 该库提供了访问PDF签名字段及其底层元数据的功能。以下是一个最小示例,用于检查PDF中是否存在数字签名,并读取其状态:
import sys
from pypdf import PdfReader
def check_pdf_signatures(path: str) -> None:
reader = PdfReader(path)
sig_fields = [
name for name, field in (reader.get_fields() or {}).items()
if field.get("/FT") == "/Sig"
]
if not sig_fields:
print("No digital signature fields found.")
return
print(f"Found {len(sig_fields)} signature field(s):")
for name in sig_fields:
sig_obj = reader.get_fields()[name]
sig_dict = sig_obj.get("/V")
if not sig_dict:
print(f" {name}: field present but unsigned")
continue
signer_name = sig_dict.get("/Name", "Unknown")
signing_time = sig_dict.get("/M", "No timestamp")
reason = sig_dict.get("/Reason", "")
location = sig_dict.get("/Location", "")
print(f" {name}:")
print(f" Signer: {signer_name}")
print(f" Time: {signing_time}")
if reason:
print(f" Reason: {reason}")
if location:
print(f" Location: {location}")
if __name__ == "__main__":
check_pdf_signatures(sys.argv[1])
此方法直接从PDF结构中读取签名元数据。要完成完整的加密验证——确认哈希值并验证证书链——请使用 pyhanko 或 endesive,这两个库均封装了PKCS#7验证层。
使用qpdf命令行工具
在没有Python环境的情况下快速检查文件:
# Show encryption and signature info
qpdf --show-encryption input.pdf
# Full JSON output with signature details
qpdf --json input.pdf | python3 -m json.tool | grep -A 20 '"sig"'
qpdf 在CI流水线或shell脚本中非常有用,因为设置Python虚拟环境会带来不必要的开销。
常见验证场景
来自客户的合同 ——检查签名的有效性状态和证书颁发者。来自不受信任根证书(如自签名证书)的有效签名无法提供外部保障。你所信任的是生成密钥的那个人。
政府和监管文件 ——这些文件通常使用全国范围内的可信证书颁发机构。应验证证书链最终锚定到预期的根CA,而不仅仅是签名显示为“有效”。
银行对账单和发票 ——许多文件是批量签名的,使用的是组织机构签发的文档签名证书。签名者名称将反映机构名称,而非具体员工姓名。
无需编写代码即可验证
如果你需要在不设置开发环境的情况下快速检查PDF签名,可以使用 IO Tools PDF签名检查器 你可以上传文件并立即查看签名详情——适用于一次性验证或在构建生产流程前测试示例文件。
