通读以下参考资料:
- iText 数字签名白皮书和C# 示例。(特别是第 4 章)对于那些感兴趣的人,PDF 签名过程的另一个伟大而简洁的总结。
- CAPICOM 文档。
- 在线示例/问题here和iText邮件列表档案,例如here和here。
哈希码:
BouncyCastle.X509Certificate[] chain = Utils.GetSignerCertChain();
reader = Utils.GetReader();
MemoryStream stream = new MemoryStream();
using (var stamper = PdfStamper.CreateSignature(reader, stream, '\0'))
{
PdfSignatureAppearance sap = stamper.SignatureAppearance;
sap.SetVisibleSignature(
new Rectangle(36, 740, 144, 770),
reader.NumberOfPages,
"SignatureField"
);
sap.Certificate = chain[0];
sap.SignDate = DateTime.Now;
sap.Reason = "testing web context signatures";
PdfSignature pdfSignature = new PdfSignature(
PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED
);
pdfSignature.Date = new PdfDate(sap.SignDate);
pdfSignature.Reason = sap.Reason;
sap.CryptoDictionary = pdfSignature;
Dictionary<PdfName, int> exclusionSizes = new Dictionary<PdfName, int>();
exclusionSizes.Add(PdfName.CONTENTS, SIG_BUFFER * 2 + 2);
sap.PreClose(exclusionSizes);
Stream sapStream = sap.GetRangeStream();
byte[] hash = DigestAlgorithms.Digest(
sapStream,
DigestAlgorithms.SHA256
);
// is this needed?
PdfPKCS7 sgn = new PdfPKCS7(
null, chain, DigestAlgorithms.SHA256, true
);
byte[] preSigned = sgn.getAuthenticatedAttributeBytes(
hash, sap.SignDate, null, null, CryptoStandard.CMS
);
var hashedValue = Convert.ToBase64String(preSigned);
}
只是一个简单的测试——在初始页面请求时创建一个虚拟的 Pdf 文档,计算哈希,并放入一个隐藏的输入字段 Base64 编码。(hashedValue
以上)
然后在客户端使用 CAPICOM 发布表单并获取用户的签名响应:
PdfSignatureAppearance sap = (PdfSignatureAppearance)TempData[TEMPDATA_SAP];
PdfPKCS7 sgn = (PdfPKCS7)TempData[TEMPDATA_PKCS7];
stream = (MemoryStream)TempData[TEMPDATA_STREAM];
byte[] hash = (byte[])TempData[TEMPDATA_HASH];
byte[] originalText = (Encoding.Unicode.GetBytes(hashValue));
// Oid algorithm verified on client side
ContentInfo content = new ContentInfo(new Oid("RSA"), originalText);
SignedCms cms = new SignedCms(content, true);
cms.Decode(Convert.FromBase64String(signedValue));
// CheckSignature does not throw exception
cms.CheckSignature(true);
var encodedSignature = cms.Encode();
/* tried this too, but no effect on result
sgn.SetExternalDigest(
Convert.FromBase64String(signedValue),
null,
"RSA"
);
byte[] encodedSignature = sgn.GetEncodedPKCS7(
hash, sap.SignDate, null, null, null, CryptoStandard.CMS
);
*/
byte[] paddedSignature = new byte[SIG_BUFFER];
Array.Copy(encodedSignature, 0, paddedSignature, 0, encodedSignature.Length);
var pdfDictionary = new PdfDictionary();
pdfDictionary.Put(
PdfName.CONTENTS,
new PdfString(paddedSignature).SetHexWriting(true)
);
sap.Close(pdfDictionary);
所以现在我不确定我是在搞乱哈希部分、签名部分还是两者兼而有之。在上面的签名代码片段和客户端代码(未显示)中,我正在调用我认为是签名验证代码,但这也可能是错误的,因为这对我来说是第一次。打开 PDF 时收到臭名昭著的“文档自签名后已被更改或损坏”无效签名消息。
可以在此处找到客户端代码(不是我编写的)。源有一个变量命名错误,已更正。作为参考,CAPICOM 文档说签名响应采用 PKCS#7 格式。
编辑 2015-03-12:
经过@mkl 的一些不错的指导和更多的研究,似乎 CAPICOM在这种情况下实际上是不可用的。虽然没有清楚地记录,(还有什么新的?)根据here和here,CAPICOM期望一个utf16字符串(Encoding.Unicode
在.NET中)作为输入来创建一个数字签名。如果长度是奇数,它会从那里填充或截断(取决于前一句中正确的源)它接收到的任何数据。即,如果PdfSignatureAppearance.GetRangeStream()返回的长度为奇数,则签名创建将始终失败。也许我应该创造一个我很幸运Stream
选项:如果远程流长度为偶数,则标记,InvalidOperationException
如果奇数则抛出。(可悲的幽默尝试)
作为参考,这里是测试项目。
编辑 2015-03-25:
为了结束这个循环,这里有一个 VS 2013 ASP.NET MVC 项目的链接。可能不是最好的方法,但它确实为问题提供了一个完全有效的解决方案。由于 CAPICOM 的奇怪且不灵活的签名实现,如上所述,如果PdfSignatureAppearance.GetRangeStream()的返回值(再次,Stream.Length
)是奇数,则可能的解决方案可能需要第二次传递和注入额外字节的方法。我打算通过填充 PDF 内容来尝试漫长而艰难的方法,但幸运的是,一位同事发现填充 PDF 内容要容易得多PdfSignatureAppearance.Reason
。要求第二次通过 iText[Sharp] 做某事并非史无前例 - 例如为文档页眉/页脚添加第 x 页,共 y 页。