数字签名是 PDF 文档安全体系中的核心机制,它通过非对称加密技术保证文档的完整性、真实性和不可抵赖性。在合同审批、法律文书归档、电子发票流转等场景中,数字签名能够证明文档自签署以来未被篡改,并明确签署者身份。然而,在实际业务流程中,签名操作并非一劳永逸------当证书过期、签署人变更或文档需要重新修订时,往往需要移除已有的数字签名并重新签署。通过 Python 编程方式管理 PDF 数字签名,可以实现签名添加、验证和移除的自动化,特别是在批量签署合同或集成到审批系统时,编程方式比手动操作工具更加高效可靠。本文将介绍如何使用 Python 在 PDF 文档中添加可见和不可见数字签名、配置时间戳和文档权限、验证签名有效性,以及移除已有的数字签名。
环境准备
本文使用 Spire.PDF for Python 库来操作 PDF 数字签名。该库提供了 PdfOrdinarySignatureMaker 和 Security_PdfSignature 等签名工具类,支持 PFX 证书加载、签名外观自定义和时间戳配置。通过以下命令安装:
bash
pip install Spire.PDF
安装完成后,在脚本中导入所需模块即可开始使用。数字签名需要准备一个 PFX/P12 格式的证书文件(含私钥)以及对应的密码。
添加可见数字签名
可见签名会在 PDF 页面上显示一个签名区域,包含签署者姓名、位置、联系方式和签名图片等信息。这种签名方式适合需要直观展示签署信息的正式文档。PdfOrdinarySignatureMaker 类负责加载证书并生成签名对象,PdfSignatureAppearance 类则用于控制签名区域的外观呈现。
python
from spire.pdf import *
inputFile = "/Sample.pdf"
inputFile_pfx = "/sign.pfx"
inputImage = "/signature.png"
outputFile = "DigitalSignature.pdf"
# 加载 PDF 文档
doc = PdfDocument()
doc.LoadFromFile(inputFile)
# 使用 PDF 文档和 PFX 证书创建签名生成器
signatureMaker = PdfOrdinarySignatureMaker(doc, inputFile_pfx, "e-iceblue")
# 配置签名属性
signature = signatureMaker.Signature
signature.Name = "Alice Chen"
signature.ContactInfo = "028-81705109"
signature.Location = "Chengdu, China"
signature.Reason = "The certificate is used to sign the document."
# 创建签名外观
appearance = PdfSignatureAppearance(signature)
appearance.NameLabel = "Signer: "
appearance.ContactInfoLabel = "ContactInfo: "
appearance.LocationLabel = "Location: "
appearance.ReasonLabel = "Reason: "
appearance.SignatureImage = PdfImage.FromFile(inputImage)
appearance.GraphicMode = GraphicMode.SignImageAndSignDetail
appearance.SignImageLayout = SignImageLayout.none
# 将签名应用到第一页的指定位置
signatureMaker.MakeSignature("Signer:", doc.Pages.get_Item(0), 90.0, 550.0, 270.0, 90.0, appearance)
# 保存文档
doc.SaveToFile(outputFile)
doc.Close()

上述代码中,PdfOrdinarySignatureMaker 的构造函数接收三个参数:PDF 文档对象、PFX 证书路径和证书密码。signature.Name、signature.Location 等属性用于填充签名元数据,这些信息会嵌入到签名字典中,并在签名外观区域显示。PdfSignatureAppearance 类控制签名的视觉呈现------GraphicMode.SignImageAndSignDetail 表示同时显示签名图片和签署详情,SignImageLayout.none 表示图片不单独占据一个区域,而是与文字信息混合排列。MakeSignature() 方法的参数依次为签名名称、目标页面、X 坐标、Y 坐标、宽度和高度,签名框将定位在第一页的指定位置。
添加不可见数字签名
某些场景下不需要在页面上显示签名外观,只需在文档内部嵌入数字签名即可。不可见签名不会在页面上产生可见的签名框,但仍具有完整的法律效力。这种方式适合批量签署大量文档,或在文档布局已定稿、不希望添加额外视觉元素的情况下使用。
python
from spire.pdf import *
inputFile = "/Sample.pdf"
inputFile_pfx = "/sign.pfx"
outputFile = "InvisibleSignature.pdf"
# 加载 PDF 文档
doc = PdfDocument()
doc.LoadFromFile(inputFile)
# 创建签名生成器
signatureMaker = PdfOrdinarySignatureMaker(doc, inputFile_pfx, "e-iceblue")
# 生成不可见签名,指定签名名称
signatureMaker.MakeSignature("signName")
# 保存文档
doc.SaveToFile(outputFile)
doc.Close()

与可见签名不同,不可见签名调用的是 MakeSignature() 的单参数重载版本,只接收签名名称作为参数,不传入页面和坐标信息,也不创建 PdfSignatureAppearance 对象。签名数据仍然完整嵌入到 PDF 的 AcroForm 字段中,任何支持数字签名的 PDF 阅读器都可以在签名面板中查看和验证。
添加带时间戳和权限控制的签名
在对签名时效性要求较高的场景中,可以引入可信时间戳(Trusted Timestamp)来证明签名发生的确切时间。时间戳由可信第三方机构(TSA)签发,即使签名证书后续过期,时间戳仍能证明文档在证书有效期内已完成签署。同时,还可以通过 DocumentPermissions 属性限制签署后文档的修改权限,例如禁止修改文档内容但允许填写表单。
python
from spire.pdf import *
inputFile = "./Data/SampleB_1.pdf"
inputFile_pfx = "./Data/gary.pfx"
inputImage = "./Data/E-iceblueLogo.png"
outputFile = "SignedByTimestamp.pdf"
# 加载 PDF 文档
doc = PdfDocument()
doc.LoadFromFile(inputFile)
# 创建数字签名对象
signature = Security_PdfSignature(doc, doc.Pages.get_Item(0), inputFile_pfx, "e-iceblue", "signature")
# 设置签名框位置和尺寸
signature.Bounds = RectangleF(PointF(90.0, 550.0), SizeF(180.0, 90.0))
# 配置签名详情
signature.NameLabel = "Digitally signed by:Gary"
signature.LocationInfoLabel = "Location:"
signature.LocationInfo = "CN"
signature.ReasonLabel = "Reason:"
signature.Reason = "Ensure authenticity"
signature.ContactInfoLabel = "Contact Number: "
signature.ContactInfo = "028-81705109"
# 设置文档权限:允许填写表单但禁止修改内容
signature.DocumentPermissions = PdfCertificationFlags.AllowFormFill.value | PdfCertificationFlags.ForbidChanges.value
# 设置签名图片
signature.GraphicsMode = Security_GraphicMode.SignImageAndSignDetail
signature.SignImageSource = PdfImage.FromFile(inputImage)
# 配置时间戳服务器 URL
url = "https://freetsa.org/tsr"
signature.ConfigureTimestamp(url)
# 保存文档
doc.SaveToFile(outputFile)
doc.Close()
这段代码使用了 Security_PdfSignature 类(与前面的 PdfOrdinarySignatureMaker 是两种不同的签名 API 风格),它直接在构造函数中传入文档、页面、证书路径、密码和签名名称。DocumentPermissions 通过位运算组合多个权限标志------AllowFormFill 允许用户填写表单字段,ForbidChanges 禁止对文档内容进行任何修改。ConfigureTimestamp(url) 方法接收一个 TSA 服务器地址,签名过程中会自动向该服务器发送请求获取时间戳令牌并嵌入签名中。使用 | 运算符可以组合多个权限标志,这与 C# 中的枚举位运算法则一致。
验证签名有效性和文档完整性
添加签名后,在后续流程中通常需要验证签名的有效性和文档是否被修改。签名验证包括检查证书链是否可信、签名哈希是否匹配等。通过 PdfFormWidget 可以访问 PDF 中的表单字段,签名字段是其中的一种特殊类型,通过 PdfSignatureFieldWidget 进行操作。
python
from spire.pdf import *
inputFile = "DigitalSignature.pdf.pdf"
outputFile = "VerifySignature.txt"
# 加载已签名的 PDF 文档
doc = PdfDocument()
doc.LoadFromFile(inputFile)
# 获取文档中的表单
pdfform = doc.Form
formWidget = PdfFormWidget(pdfform)
texts = []
# 遍历所有表单字段
if formWidget.FieldsWidget.Count > 0:
for i in range(formWidget.FieldsWidget.Count):
field = formWidget.FieldsWidget.get_Item(i)
if isinstance(field, PdfField):
# 将字段转换为签名字段
signatureField = PdfSignatureFieldWidget(field)
fullName = signatureField.FullName
# 验证签名
valid = doc.VerifySignature(fullName)
if valid:
text = "签名 '" + fullName + "' 有效\n"
else:
text = "签名 '" + fullName + "' 无效\n"
texts.append(text)
# 输出验证结果
for t in texts:
print(t)
doc.Close()

上述代码通过 doc.Form 获取 PDF 的 AcroForm 表单对象,再包装为 PdfFormWidget 以访问字段集合。遍历字段时,使用 isinstance(field, PdfField) 判断字段类型,然后转换为 PdfSignatureFieldWidget 获取签名字段的全名。doc.VerifySignature(fullName) 方法执行实际的签名验证逻辑,返回布尔值表示签名是否有效。
要检查已签署的文档是否在签名后被修改,可以使用 Signature.VerifyDocModified() 方法:
python
from spire.pdf import *
inputFile = "./Data/ExtractImageFromSignature.pdf"
doc = PdfDocument()
doc.LoadFromFile(inputFile)
pdfform = doc.Form
formWidget = PdfFormWidget(pdfform)
if formWidget.FieldsWidget.Count > 0:
for i in range(formWidget.FieldsWidget.Count):
field = formWidget.FieldsWidget.get_Item(i)
if isinstance(field, PdfField):
signatureField = PdfSignatureFieldWidget(field)
signatureOne = signatureField.Signature
modified = signatureOne.VerifyDocModified()
if modified:
print("文档在签名后已被修改")
else:
print("文档在签名后未被修改")
break
doc.Close()
VerifyDocModified() 方法通过比较签名时记录的文档哈希值与当前文档状态,判断文档内容是否发生了变化。如果文档在签名后被修改,签名虽然在技术层面仍然存在,但其法律效力会受到质疑。
移除数字签名
当证书过期、签署流程变更或文档需要重新编辑时,可能需要移除已有的数字签名。PDF 中的数字签名以 AcroForm 签名字段的形式存储,通过 PdfFormWidget.FieldsWidget 集合可以访问并移除签名字段。移除签名字段后,签名数据将一并从文档中删除。
python
from spire.pdf import *
inputFile = "./Data/SignedDocument.pdf"
outputFile = "RemoveSignature.pdf"
# 加载已签名的 PDF 文档
doc = PdfDocument()
doc.LoadFromFile(inputFile)
# 获取表单对象
pdfform = doc.Form
formWidget = PdfFormWidget(pdfform)
if formWidget.FieldsWidget.Count > 0:
i = 0
while i < formWidget.FieldsWidget.Count:
field = formWidget.FieldsWidget.get_Item(i)
# 判断是否为签名字段
if isinstance(field, PdfField):
signatureField = PdfSignatureFieldWidget(field)
# 移除该签名字段
formWidget.FieldsWidget.Remove(field)
break
i += 1
# 保存文档
doc.SaveToFile(outputFile)
doc.Close()
上述代码遍历表单字段集合,找到第一个签名字段后通过 FieldsWidget.Remove() 方法将其移除。需要注意的是,移除字段后集合的索引会立即重排,因此在循环删除多个字段时,建议使用 while 循环配合手动索引管理,或从末尾倒序删除,避免索引越界。
如果要移除所有数字签名,可以修改循环逻辑:
python
# 移除所有签名字段
i = 0
while i < formWidget.FieldsWidget.Count:
field = formWidget.FieldsWidget.get_Item(i)
if isinstance(field, PdfField):
formWidget.FieldsWidget.Remove(field)
# 移除后不递增索引,因为后续字段前移
else:
i += 1
这种写法在每次移除字段后不递增索引 i,因为被移除字段后面的所有字段会自动前移填补空位。只有当当前字段不是签名字段时才递增索引,确保所有字段都被检查到。
实用技巧
提取签名中的图片
某些签名外观包含签名图片(如手写签名扫描件),在审核或归档场景中可能需要提取这些图片。PdfFormWidget 提供了 ExtractSignatureAsImages 属性,可以直接获取所有签名中的图片对象。
python
i = 0
for image in formWidget.ExtractSignatureAsImages:
filename = "SignatureImage-" + str(i) + ".png"
image.Save(filename)
i += 1
这种方法无需手动遍历签名字段,库内部会自动处理签名图片的解析和提取。
启用 OCSP 在线证书验证
除了时间戳,还可以通过 OCSP(在线证书状态协议)实时验证签名证书的有效性。Security_PdfSignature 类提供了 ConfigureHttpOCSP() 方法来配置 OCSP 验证:
python
signature.ConfigureHttpOCSP(None, None)
传入 None 参数表示使用证书中内置的 OCSP 服务地址。启用 OCSP 后,签名验证时会在线查询证书是否被吊销,增强签名可信度。
批量签署文档
在合同批量签署场景中,可以将签名逻辑封装为函数,结合文件遍历实现批量处理:
python
import os
def sign_pdf(input_path, output_path, pfx_path, pfx_password):
doc = PdfDocument()
doc.LoadFromFile(input_path)
signatureMaker = PdfOrdinarySignatureMaker(doc, pfx_path, pfx_password)
signatureMaker.MakeSignature("batchSign")
doc.SaveToFile(output_path)
doc.Close()
# 遍历目录批量签署
input_dir = "./contracts/"
output_dir = "./signed/"
for filename in os.listdir(input_dir):
if filename.endswith(".pdf"):
sign_pdf(
os.path.join(input_dir, filename),
os.path.join(output_dir, "signed_" + filename),
"./cert/gary.pfx",
"e-iceblue"
)
这种封装方式使签名逻辑可复用,在处理大量合同或发票时显著减少重复代码。
总结
通过 Python 自动化管理 PDF 数字签名,能极大地赋能合同审批与电子归档场景。结合签名验证、防篡改与时间戳机制,可高效搭建文档信任链,在兼顾签署效率的同时,全方位保障文档的真实性与合规性。