首先,正如 cmets 中讨论的问题和Bharat's answer:
在应用签名后需要更新签名外观表明签名解决方案的架构不好。
在目前的情况下,这种糟糕的架构似乎是要求的结果(“外观必须包含证书信息”与“证书在签名前不可用”相结合)。尽管如此,这是一个糟糕的架构,应该在审查和修改需求后加以改进。
但在良性情况下确实可以更新签名外观:如果现有签名允许“表单填写和注释更改”并且不完全锁定相应的签名字段,外观的签名可以在增量更新中更新,而不会使签名失效(但验证器可能会警告更改)。
更新通用 PDF 签名
PDF 规范没有具体定义签名字段的外观结构,通用解决方案只需将每个签名字段小部件注释的外观流替换为新的。这可以使用 iText 5.5.x for .Net 来完成:
using (PdfReader pdfReader = new PdfReader(SRC))
using (PdfStamper pdfStamper = new PdfStamper(pdfReader, new FileStream(DEST, FileMode.Create, FileAccess.Write), '\0', true))
{
AcroFields acroFields = pdfStamper.AcroFields;
foreach (String signatureName in acroFields.GetSignatureNames())
{
PdfPKCS7 pkcs7 = acroFields.VerifySignature(signatureName);
X509Certificate signerCert = pkcs7.SigningCertificate;
String signerName = CertificateInfo.GetSubjectFields(signerCert).GetField("CN");
PdfAppearance appearance = PdfAppearance.CreateAppearance(pdfStamper.Writer, 100, 100);
ColumnText columnText = new ColumnText(appearance);
Chunk chunk = new Chunk();
chunk.SetSkew(0, 12);
chunk.Append("Signed by:");
columnText.AddElement(new Paragraph(chunk));
chunk = new Chunk();
chunk.SetTextRenderMode(PdfContentByte.TEXT_RENDER_MODE_FILL_STROKE, 1, BaseColor.BLACK);
chunk.Append(signerName);
columnText.AddElement(new Paragraph(chunk));
columnText.SetSimpleColumn(0, 0, 100, 100);
columnText.Go();
PdfDictionary appDict = new PdfDictionary();
appDict.Put(PdfName.N, appearance.IndirectReference);
AcroFields.Item field = acroFields.GetFieldItem(signatureName);
for (int i = 0; i < field.Size; i++)
{
PdfDictionary widget = field.GetWidget(i);
PdfArray rect = widget.GetAsArray(PdfName.RECT);
float x = Math.Min(rect.GetAsNumber(0).FloatValue, rect.GetAsNumber(0).FloatValue);
float y = Math.Min(rect.GetAsNumber(1).FloatValue, rect.GetAsNumber(3).FloatValue);
widget.Put(PdfName.RECT, new PdfArray(new float[] { x, y, x + 100, y + 100 }));
}
field.WriteToAll(PdfName.AP, appDict, AcroFields.Item.WRITE_WIDGET);
field.MarkUsed(acroFields, AcroFields.Item.WRITE_WIDGET);
}
}
如您所见,代码从签名者证书中提取主题的公用名并将其(以"Signed by:" 行作为前缀)写入新外观。如果您在替换外观中需要其他数据,只需相应地更改添加到columnText 和/或appearance 的数据即可。
此外,代码将所有外观替换为 100×100 大小的新外观。当然,您也可以根据自己的要求进行调整。
这实质上是从this answer 到 C# 的代码移植。
使用 Adobe 特定层更新 PDF 签名
Adobe Acrobat Reader 使用特定的方案来构建其签名外观,甚至为根据该方案的旧版本构建的签名添加了某些功能。如上所述,PDF 规范没有规定任何此类方案;实际上它甚至禁止这样的功能,cf。 this answer.
尽管如此,尤其是来自印度的许多堆栈溢出问题似乎表明,客户通常需要遵循该过时方案的签名。
如果遵循此方案,则外观本身被构造为表单 XObjects 的层次结构,特别是一组所谓的“层”n0 到 n4,其中n2 是签名者应在其上应用其身份的层。
上面的通用解决方案可以修改如下以符合该方案:
using (PdfReader pdfReader = new PdfReader(SRC))
using (PdfStamper pdfStamper = new PdfStamper(pdfReader, new FileStream(DEST, FileMode.Create, FileAccess.Write), '\0', true))
{
AcroFields acroFields = pdfStamper.AcroFields;
foreach (String signatureName in acroFields.GetSignatureNames())
{
PdfPKCS7 pkcs7 = acroFields.VerifySignature(signatureName);
X509Certificate signerCert = pkcs7.SigningCertificate;
String signerName = CertificateInfo.GetSubjectFields(signerCert).GetField("CN");
AcroFields.Item field = acroFields.GetFieldItem(signatureName);
for (int i = 0; i < field.Size; i++)
{
PdfDictionary widget = field.GetWidget(i);
Rectangle rect = PdfReader.GetNormalizedRectangle(widget.GetAsArray(PdfName.RECT));
PdfAppearance appearance = PdfAppearance.CreateAppearance(pdfStamper.Writer, rect.Width, rect.Height);
ColumnText columnText = new ColumnText(appearance);
Chunk chunk = new Chunk();
chunk.SetSkew(0, 12);
chunk.Append("Signed by:");
columnText.AddElement(new Paragraph(chunk));
chunk = new Chunk();
chunk.SetTextRenderMode(PdfContentByte.TEXT_RENDER_MODE_FILL_STROKE, 1, BaseColor.BLACK);
chunk.Append(signerName);
columnText.AddElement(new Paragraph(chunk));
columnText.SetSimpleColumn(0, 0, rect.Width, rect.Height - 15);
columnText.Go();
PdfDictionary xObjects = GetAsDictAndMarkUsed((PdfStamperImp)pdfStamper.Writer, widget, PdfName.AP, PdfName.N, PdfName.RESOURCES, PdfName.XOBJECT, PdfName.FRM, PdfName.RESOURCES, PdfName.XOBJECT);
xObjects.Put(PdfName.N2, appearance.IndirectReference);
}
}
}
使用以下辅助方法:
PdfDictionary GetAsDictAndMarkUsed(PdfStamperImp writer, PdfDictionary dictionary, params PdfName[] names)
{
PRIndirectReference reference = null;
foreach (PdfName name in names)
{
if (dictionary != null)
{
dictionary = dictionary.GetDirectObject(name) as PdfDictionary;
if (dictionary != null)
{
if (dictionary.IndRef != null)
reference = dictionary.IndRef;
}
}
}
if (reference != null)
writer.MarkUsed(reference);
return dictionary;
}
(请注意:此代码假定签名遵循 Adobe 方案;如果您不确定您的输入是否符合,请添加一些健全性检查并默认使用上述通用解决方案。)