1

通读以下参考资料:

哈希码:

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在这种情况下实际上是不可用的。虽然没有清楚地记录,(还有什么新的?)根据herehere,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 页

4

1 回答 1

1

PdfPkcs7的使用

服务器端代码在计算范围流摘要之后和将数据转发到网页之前包含此块:

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);

在手头的情况下,这不是必需的。仅当您使用的外部签名 API 仅返回签名摘要时才需要它;在这种情况下,PdfPKCS7实例会构建 CMS/PKCS#7 签名容器。另一方面,您使用您知道的 API

CAPICOM 文档说签名响应采用 PKCS#7 格式。

因此,您不需要并且(更重要的是)不能使用PdfPKCS7实例。

sign.js 有什么标志

服务器端hash变量的内容已经是要签名的数据的哈希摘要值。因此,前端,即那里使用的sign.js,不能再次对其进行哈希处理以获取消息摘要属性值以放入签名中。

但是 IE 的 sign.js 签名方法最终会执行

var signedData = new ActiveXObject("CAPICOM.SignedData");

// Set the data that we want to sign
signedData.Content = src;

SignedData.Content,另一方面,被记录为

内容 读/写要签名的数据。

msdn:“SignedData 对象”

所以来自后端的散列被用作要签名的数据,而不是要签名的数据的散列,你确实散列了两次,所以那里有错误的散列值。

因此,看起来您必须传输整个远程流,这并不实际......

“但曾经有使用 CAPICOM 签署样本......”

事实上,一些旧的 iTextSharp(4.x 版)签名示例使用了 CAPICOM。但该代码之所以有效,是因为它创建了 PDF 签名类型adbe.pkcs7.sha1的签名,其中范围流的 SHA1 哈希确实是嵌入并由 PKCS#7 签名的数据

这不再是真正的选择,因为

  • 它需要使用在严重情况下无效的 SHA1,并且
  • 至少自 ISO 32000-1 (2008) 以来一直不鼓励使用它,并将在 ISO 32000-2(开发中)中正式弃用。
于 2015-03-13T14:31:20.640 回答