数字签名技术在确保PDF文档的来源认证和完整性方面发挥着重要作用。本文将展示如何对PDF文档应用数字签名,以及如何验证这些签名的有效性。
数字签名可以用于验证PDF文档的来源(谁签署的?)以及确保PDF文档的完整性(签署后文档是否被更改?)。
考虑以下表单:
图1:带有字段和两个空白签名字段的PDF表单
此表单包含两个部分的表单字段;一个用于学生,一个用于教师。通常,首先由学生填写他/她的部分并签署文档。这可以通过以下方式编程实现:
C++
using
(FileStream sourceFile =
new
FileStream(
"form.pdf", FileMode.Open, FileAccess.Read))
{
// 打开表单
Document document = new Document(sourceFile);
// 填写数据字段
TextField projectNr = document.Fields["projectNumber"] as TextField;
projectNr.Value = "FF-235";
TextField sName = document.Fields["studentName"] as TextField;
sName.Value = "Bob Stapleton";
TextField sDate = document.Fields["studentDate"] as TextField;
sDate.Value = "April 18, 2007";
// 检索签名字段
SignatureField sigField = document.Fields["studentSignature"] as SignatureField;
// 打开证书存储
Pkcs12Store store = null;
using (FileStream storeFile = new FileStream("ChrisSharp.pfx", FileMode.Open, FileAccess.Read))
{
store = new Pkcs12Store(storeFile, "Sample");
}
// 让工厂决定使用哪种类型
SignatureHandler handler = StandardSignatureHandler.Create(store);
// 将处理器与签名字段关联
sigField.SignatureHandler = handler;
// 设置可选信息
sigField.ContactInfo = "+31 (0)77 4748677";
sigField.Location = "The Netherlands";
sigField.Reason = "I hereby declare!";
// 保存已签名的文档 - 在保存时,处理器将被调用以签署保存的内容
using (FileStream outFile = new FileStream("signedByStudent.pdf", FileMode.Create, FileAccess.ReadWrite))
{
document.Write(outFile);
}
}
执行上述代码并在PDF阅读器中打开PDF文档后,文档将如下所示:
图2:应用第一个签名后
请注意PDF阅读器显示的问号。这意味着证书尚未被客户端机器信任。这仅仅是将证书添加到信任存储的问题。
接下来,教师将审查学生的数据,然后填写最终字段并签署文档。这可以通过以下方式编程实现:
C++
using
(FileStream sourceFile =
new
FileStream(
"..\..\signedByStudent.pdf", FileMode.Open, FileAccess.Read))
{
// 打开表单
Document document = new Document(sourceFile);
// 填写数据字段
TextField tName = document.Fields["teacherName"] as TextField;
tName.Value = "Max Boulton";
TextField tDate = document.Fields["teacherDate"] as TextField;
tDate.Value = "April 18, 2007";
// 检索签名字段
SignatureField sigField = document.Fields["teacherSignature"] as SignatureField;
// 打开证书存储
Pkcs12Store store = null;
using (FileStream storeFile = new FileStream("../..MaxBoulton.pfx", FileMode.Open, FileAccess.Read))
{
store = new Pkcs12Store(storeFile, "teacherpassword");
}
// 让工厂决定使用哪种类型
SignatureHandler handler = StandardSignatureHandler.Create(store);
// 将处理器与签名字段关联
sigField.SignatureHandler = handler;
// 设置可选信息
sigField.ContactInfo = "+31 (0)77 4748677";
sigField.Location = "The Netherlands";
sigField.Reason = "I hereby declare!";
// 保存已签名的文档 - 在保存时,处理器将被调用以签署保存的内容
using (FileStream outFile = new FileStream("../..\signedByTeacher.pdf", FileMode.Create, FileAccess.ReadWrite))
{
document.Write(outFile, DocumentWriteMode.AppendUpdate);
}
}
如果查看代码,它实际上与之前的代码示例相同。唯一的显著区别是向Document.Write方法传递了一个额外的参数:
DocumentWriteMode.AppendUpdate
。这指示PDFKit.NET将新字段数据和签名保存为所谓的“更新”。将在下一节中讨论这一点。
执行上述代码并在PDF阅读器中打开PDF文档后,文档将如下所示:
图3:应用第二个签名后
请注意第一个签名的图标已更改为警告标志。这表明“自签署以来文档已更新”。这正是情况。
请注意,当保存第二个签名时,向Document.Write传递了一个额外的参数,即DocumentWriteMode.AppendUpdate。这指示PDFKit.NET将新字段数据和签名保存为“更新”。这意味着原始PDF数据完全保持不变,更改被附加。下面的图示说明了这一点。
图4:PDF更新
因此,第一个签名仍然有效,因为被签署的确切数据没有变化;只是添加了更新。
因此,在保存更新后,现在实际上有两个版本的文档;一个由学生签署,一个由教师签署。检索给定签名应用的确切文档是有用的。显然,签名者只为那个版本发誓,而不是为之后创建的版本。
给定一个文档,可以枚举所有更新或版本的文档,并将副本保存到磁盘,如下所示:
C++
using
(FileStream sourceFile =
new
FileStream(
"..\..\signedByTeacher.pdf", FileMode.Open, FileAccess.Read))
{
// 打开PDF文档
Document document = new Document(sourceFile);
// 计算更新的数量
Console.WriteLine("This document has {0} updates.", document.Updates.Count);
// 将每个更新保存为新的PDF文档
foreach (Update update in document.Updates)
{
string name = string.Format(@"..\..\signedByTeacher_{0}.pdf", update.Index);
using (FileStream updateFile = new FileStream(name, FileMode.Create, FileAccess.Write))
{
update.Write(updateFile);
}
}
}
但也许更有趣的是,可以打开一个已签名的文档,并且对于每个签名字段,可以检索已签名的更新。以下代码示例枚举了所有签名字段,并保存了已签名的更新。
C++
using
(FileStream sourceFile =
new
FileStream(
"..\..\signedByTeacher.pdf", FileMode.Open, FileAccess.Read))
{
// 打开表单
Document document = new Document(sourceFile);
foreach (Field field in document.Fields)
{
// 这是一个签名字段吗?
SignatureField sigField = field as SignatureField;
if (null != sigField)
{
// 它被签署了吗?
if (sigField.IsSigned)
{
// 保存更新并按字段命名
string name = string.Format(@"..\..\{0}.pdf", sigField.FullName);
using (FileStream updateFile = new FileStream(name, FileMode.Create, FileAccess.Write))
{
sigField.SignedUpdate.Write(updateFile);
}
}
}
}
}
执行此代码后,将保存两个新的PDF文档:studentSignature.pdf和teacherSignature.pdf。每个文档显示由相应字段签署的版本。
using
(FileStream inFile =
new
FileStream(
"..\..\signedByTeacher.pdf", FileMode.Open, FileAccess.Read))
{
// 打开表单
Document document = new Document(inFile);
foreach (Field field in document.Fields)
{
// 这是一个签名字段吗?
SignatureField sigField = field as SignatureField;
if (null != sigField)
{
Console.WriteLine("Field '{0}'", sigField.FullName);
// 它被签署了吗?
if (sigField.IsSigned)
{
// 验证,基于默认处理器。
bool verified = sigField.Verify();
Console.WriteLine("-- {0}", verified ? "Verified" : "Not verified");
if (verified)
{
// 文档在签署后是否被修改?
bool modified = sigField.DocumentModifiedAfterSigning;
Console.WriteLine("-- {0}", modified ? "Modified after signing" : "Not modified after signing");
}
}
else
{
Console.WriteLine("-- Not signed", sigField.FullName);
}
}
}
}