使用外部服务和 iText 签署 PDF

Sign PDF using an external service and iText

我有这个场景。

我有一个生成 PDF 的应用程序,需要签名。

我们没有签署文档的证书,因为它们在 HSM 中,我们可以使用证书的唯一方法是使用网络服务。

此网络服务提供两个选项,发送 PDF 文档,然后 returns 签名的 pdf,或发送将被签名的散列。

第一个选项不可行,因为 PDF 签名时没有时间戳(这是一个非常重要的必要条件),所以选择第二个选项。

这是我们的代码,首先,我们得到签名外观,并计算哈希值:

PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '[=10=]', null, true);
appearance = stamper.getSignatureAppearance();
appearance.setCrypto(null, chain, null, PdfSignatureAppearance.SELF_SIGNED);
appearance.setVisibleSignature("Representant");
cal = Calendar.getInstance();
PdfDictionary dic = new PdfDictionary();
dic.put(PdfName.TYPE, PdfName.SIG);
dic.put(PdfName.FILTER, PdfName.ADOBE_PPKLITE);
dic.put(PdfName.SUBFILTER, new PdfName("adbe.pkcs7.detached"));
dic.put(PdfName.M, new PdfDate(cal));
appearance.setCryptoDictionary(dic);
HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
exc.put(PdfName.CONTENTS, Integer.valueOf(reservedSpace.intValue() * 2 + 2));
appearance.setCertificationLevel(1);
appearance.preClose(exc);

AbstractChecksum checksum = JacksumAPI.getChecksumInstance("sha1");
checksum.reset();
checksum.update(Utils.streamToByteArray(appearance.getRangeStream()));
hash = checksum.getByteArray();

至此,我们有了文档的哈希码。然后我们将哈希发送到 web 服务,我们得到签名的哈希码。

最后,我们将签名后的哈希值放入 PDF 中:

byte[] paddedSig = new byte[reservedSpace.intValue()];
System.arraycopy(signedHash, 0, paddedSig, 0, signedHash.length);

PdfDictionary dic = new PdfDictionary();
dic.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));
appearance.close(dic);

byte[] pdf = baos.toByteArray();

至此,我们得到了一个 PDF 签名,但签名无效。 Adobe 表示 "Document has been altered or corrupted since it was signed".

我认为我们在这个过程中犯了一些错误,我们不知道到底是什么。

我们感谢您对此提供的帮助,或其他方法。

谢谢。


已编辑

根据 mkl 的建议,我已经遵循了本书的 4.3.3 部分 Digital Signatures for PDF documents,现在我的代码如下:

第一部分,当我们计算哈希值时:

PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '[=12=]');
appearance = stamper.getSignatureAppearance();

appearance.setReason("Test");
appearance.setLocation("A casa de la caputeta");
appearance.setVisibleSignature("TMAQ-TSR[0].Pagina1[0].DadesSignatura[0].Representant[0]");
appearance.setCertificate(chain[0]);

PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
dic.setReason(appearance.getReason());
dic.setLocation(appearance.getLocation());
dic.setContact(appearance.getContact());
dic.setDate(new PdfDate(appearance.getSignDate()));
appearance.setCryptoDictionary(dic);

HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
exc.put(PdfName.CONTENTS, new Integer(reservedSpace.intValue() * 2 + 2));
appearance.preClose(exc);

ExternalDigest externalDigest = new ExternalDigest()
{
    public MessageDigest getMessageDigest(String hashAlgorithm) throws GeneralSecurityException
    {
        return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
    }
};

sgn = new PdfPKCS7(null, chain, "SHA256", null, externalDigest, false);
InputStream data = appearance.getRangeStream();
hash = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256"));
cal = Calendar.getInstance();

byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
sh = MessageDigest.getInstance("SHA256", "BC").digest(sh);

hashPdf = new String(Base64.encode(sh));

在第二部分,我们得到签名的哈希值,并将其放入 PDF 中:

sgn.setExternalDigest(Base64.decode(hashSignat), null, "RSA");
byte[] encodedSign = sgn.getEncodedPKCS7(hash, cal, null, null, null, CryptoStandard.CMS);
byte[] paddedSig = new byte[reservedSpace.intValue()];
System.arraycopy(encodedSign, 0, paddedSig, 0, encodedSign.length);

PdfDictionary dic2 = new PdfDictionary();
dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));

appearance.close(dic2);

byte[] pdf = baos.toByteArray();

现在,Adobe 引发了内部加密库错误。错误代码:0x2726,当我们尝试验证签名时。

如果 Web 服务仅返回签名哈希

In this point, we have the hash code of the document. Then we send the hash to the webservice, and we get the signed hash code.

Finally, we put the signed hash to the PDF:

如果网络服务只是 returns 一个签名哈希,那么您的 PDF 签名不正确:您将签名 SubFilter 设置为 adbe。pkcs7.detached。这意味着签名 Contents 必须包含一个完整的 PKCS#7 签名容器,而不仅仅是一个签名的散列。

您可能想要下载 Digital Signatures for PDF documentsBruno Lowagie 的白皮书(iText 软件) 关于使用 iText 创建和验证数字 PDF 签名的内容。它特别包含“4.3 Client/server 签名架构”部分,其中应包含您的用例。

但是网络服务returns一个成熟的CMS签名容器

根据上述解释,OP 开始使用上述白皮书第 4.3.3 节中的代码,该代码旨在使用外部生成的签名哈希进行签名。由于这也导致了 Adob​​e Reader 不满意的签名文档,他提供了使用此新代码创建的示例文档。

样本分析表明,文档中嵌入的 CMS 签名容器包含另一个 CMS 签名容器,其中应该有签名属性的签名字节(签名哈希):

2417   13:           SEQUENCE {
2419    9:             OBJECT IDENTIFIER rsaEncryption (1 2 840 113549 1 1 1)
2430    0:             NULL
         :             }
2432 5387:           OCTET STRING, encapsulates {
2436 NDEF:             SEQUENCE {
2438    9:               OBJECT IDENTIFIER signedData (1 2 840 113549 1 7 2)
2449 NDEF:               [0] {
2451 NDEF:                 SEQUENCE {

(签名算法后的OCTET STRING应包含签名字节,不能嵌入其他SignedData结构。)

这表明 Web 服务确实已经 returns 一个成熟的 CMS 容器。

对于这种情况,原始代码看起来还不错。该问题可能是由于使用了错误的散列算法(使用 SHA1 散列的原始代码)等细节引起的。

一个可能的问题:BER 编码

由 OP 提供的第一个示例中的 iText 生成的 CMS 容器中嵌入的 Web 服务中的 CMS 签名容器暗示了一个可能的问题:查看 ASN.1 转储中外部结构的大小嵌入式 CMS 容器通常是 NDEF.

这表明这些外部结构是使用不太严格的 BER(基本编码规则)创建的,而不是更严格的 DER(可分辨编码规则),因为在 DER 中禁止使用 BER 选项来启动结构而不说明其大小.

从 PDF 规范引用的 CMS 规范 (RFC 3852) 确实允许对容器的外部结构进行任何 BER 编码,另一方面 PDF 规范要求:

the value of Contents shall be a DER-encoded PKCS#7 binary data object containing the signature. The PKCS#7 object shall conform to RFC3852 Cryptographic Message Syntax.

因此,严格来说,PDF中嵌入的签名容器必须全部进行DER编码。

据我所知,只要签名容器对某些关键元素进行 DER 编码,就没有 PDF 签名验证程序会拒绝此类签名。不过,对于未来的工具,此类签名可能是一个失败点。

经过多次调试,终于找到问题所在

出于某种神秘原因,生成文档哈希的方法 被执行了两次 ,使第一个哈希(我们用来发送到服务的)无效。

对代码进行重构后,原始代码可以正常工作。

非常感谢所有帮助过我的人,尤其是mkl。