阅读以下参考文献:
-
iText 数字签名白皮书, and C# 示例。 (具体来说第4章)对于那些有兴趣的人,另一个精彩而简洁的总结PDF 签名过程。
-
CAPICOM 文档.
- 此处和 iText 邮件列表档案上的在线示例/问题,例如here and 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
above)
然后在客户端使用 CAPICOM 来 POST 表单并获取用户签名的响应:
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 and here, CAPICOM 需要一个 utf16 字符串 (Encoding.Unicode
在 .NET 中)作为创建数字签名的输入。从那里开始,它要么填充要么截断(取决于前一句中的哪个来源正确)如果长度为奇数,则接收到任何数据。 IE。签名创建将总是失败 if the Stream
由返回PdfSignatureAppearance.GetRangeStream()长度为奇数。也许我应该创建一个我很幸运选项:如果范围流长度是偶数,则进行签名,并抛出一个InvalidOperationException
如果奇数。 (悲伤的幽默尝试)
For reference, here's the test project.
编辑2015-03-25:
为了关闭这个循环,这是 VS 2013 ASP.NET MVC 项目的链接。可能不是最好的方法,但它does提供一个完全有效的解决方案到问题。由于 CAPICOM 的奇怪且不灵活的签名实现,如上所述,我们知道可能的解决方案可能需要第二次传递,并且如果返回值是,则需要注入额外字节的方法PdfSignatureAppearance.GetRangeStream()(再次,Stream.Length
) 是奇数。我本来打算通过填充 PDF 内容来尝试漫长而艰难的方法,但幸运的是,一位同事发现填充要容易得多PdfSignatureAppearance.Reason
。需要第二遍才能使用 iText[Sharp] 执行某些操作并不是史无前例的 - 例如为文档页眉/页脚添加第 x 页(共 y 页).