如何使用 iText 在 Web 上下文中使用智能卡签署 PDF?

How do I sign a PDF with a Smart Card in a web context using iText?

通读以下参考资料:

哈希码:

BouncyCastle.X509Certificate[] chain = Utils.GetSignerCertChain();
reader = Utils.GetReader();
MemoryStream stream = new MemoryStream();
using (var stamper = PdfStamper.CreateSignature(reader, stream, '[=10=]'))
{
    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 来 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 时收到臭名昭著的“文档自签名后已更改或损坏”无效签名消息。

客户端代码(不是我写的)can be found here. Source has a variable naming error, which was corrected. For reference, CAPICOM documentation says signed response is in PKCS#7 format.

编辑 2015-03-12

经过@mkl 的一些很好的指导和更多研究后,CAPICOM 似乎实际上 在此场景中不可用。尽管没有清楚地记录,(还有什么是新的?)根据 here and here, CAPICOM expects a utf16 string (Encoding.Unicode in .NET) as input to create a digital signature. From there it either pads or truncates (depending which source in previous sentence in correct) whatever data it receives if the length is an odd number. I.e. signature creation will ALWAYS FAIL if the Stream returned by PdfSignatureAppearance.GetRangeStream() 的长度是奇数。也许我应该创建一个 I'm lucky 选项:如果范围流长度是偶数则签名,如果是奇数则抛出 InvalidOperationException。 (悲伤的幽默尝试)

供参考,这里是测试项目

编辑 2015-03-25

要结束这个循环,here's a link to a VS 2013 ASP.NET MVC project. May not the be best way, but it does provide a fully working solution to the problem. Because of CAPICOM's strange and inflexible signing implementation, as described above, knew a possible solution would potentially require a second pass and a way to inject an extra byte if the return value of PdfSignatureAppearance.GetRangeStream() (again, Stream.Length) is an odd number. I was going to try the long and hard way by padding the PDF content, but luckily a co-worker found it was much easier to pad PdfSignatureAppearance.Reason. Requiring a second pass to do something with iText[Sharp], is not unprecedented - e.g. adding page x of y for a document page header/footer

使用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 您仅使用 returns 签名摘要时才需要它;在这种情况下,PdfPKCS7 实例构建 CMS/PKCS#7 签名容器。另一方面,您使用的 API 您知道

CAPICOM documentation says signed response is in PKCS#7 format.

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

sign.js是什么符号

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

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

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

// Set the data that we want to sign
signedData.Content = src;
另一方面,

SignedData.Content 记录为

Content Read/write Data to be signed.

(msdn: "SignedData object")

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

因此,看起来你必须传输整个远程流,这不太实用...

“但是曾经有使用 CAPICOM 签名的示例...”

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

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

  • 它需要使用在严重情况下无效的 SHA1,并且
  • 至少自 ISO 32000-1 (2008) 以来,不鼓励使用它,并将在 ISO 32000-2(开发中)中正式弃用。