使用外部服务和iText签署PDF

使用外部服务和iText签署PDF,itext,digital-signature,digest,Itext,Digital Signature,Digest,我有这种情况 我有一个生成PDF的应用程序,需要签名 我们没有签署文档的证书,因为它们位于HSM中,我们可以使用证书的唯一方法是使用Web服务 此Web服务提供两个选项:发送PDF文档并返回已签名的PDF,或发送将被签名的哈希 第一个选项是不可行的,因为PDF签名时没有时间戳(这是一个非常重要的必要条件),所以选择了第二个选项 这是我们的代码,首先,我们获得签名外观,并计算哈希: PdfReader reader = new PdfReader(Base64.decode(pdfB64)); r

我有这种情况

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

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

此Web服务提供两个选项:发送PDF文档并返回已签名的PDF,或发送将被签名的哈希

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

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

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

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0', 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();
PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0');
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签名,但签名无效。Adobe称“文档自签署以来已被修改或损坏”

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

我们感谢您在这方面提供的帮助,或其他方式

谢谢


编辑

根据mkl的建议,我遵循了本书的4.3.3节,现在我的代码如下所示:

第一部分,当我们计算散列时:

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

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0', 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();
PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0');
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));
现在,Adobe引发了一个内部加密库错误。错误代码:0x2726,当我们尝试验证签名时。

如果web服务仅返回已签名的哈希值 在这一点上,我们有了文档的哈希代码。然后我们将散列发送到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();
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();
如果Web服务仅返回一个签名哈希,那么您的PDF签名是不正确的:您将签名子过滤器设置为adbe.pkcs7.detached。这意味着签名内容必须包含完整的PKCS#7签名容器,而不仅仅是签名哈希

您可能想下载Bruno Lowagie(iText软件)关于使用iText创建和验证数字PDF签名的白皮书。它特别包含“4.3用于签名的客户端/服务器体系结构”一节,其中应包含您的用例

但是web服务返回一个成熟的CMS签名容器 根据上述解释,OP开始使用上述白皮书第4.3.3节中的代码,该代码旨在使用外部生成的签名哈希进行签名。由于这也导致了Adobe 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 {
(签名算法后面的八位字节字符串应该包含签名字节,而不是嵌入另一个SignedData结构。)

这表明web服务确实已经返回了一个成熟的CMS容器

对于这样的场景,原始代码看起来很正常。问题可能是由于使用了错误的散列算法(原始代码使用SHA1进行散列)等细节造成的

一个可能的问题:误码率编码 来自嵌入CMS容器中的web服务的CMS签名容器(由iText根据OP提供的第一个示例生成)提示了一个可能的问题:查看嵌入CMS容器中外部结构大小上方的ASN.1转储通常是
NDEF

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

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

内容值应为DER编码的PKCS#7二进制数据 包含签名的对象。PKCS#7对象应符合RFC3852加密消息语法

因此,严格地说,嵌入在PDF中的签名容器都需要进行DER编码


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

经过大量调试,我们终于发现了问题

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

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


非常感谢所有帮助我的人,特别是mkl。

第一个选项是不可行的,因为PDF是在没有时间戳的情况下签署的(这是一个非常重要的必要条件),-您是否考虑过应用PAdES第4部分样式的文档时间戳?如果没有选择,您是否检查过您的服务返回的集成PDF签名是否还有保留的签名空间?在这种情况下,您可以在事件发生后添加签名时间戳。好吧,这可能是这种情况下的一个选项,但在另一种情况下,我们需要以这种方式进行签名(哈希在服务器中生成,需要由客户端的用户签名)。@EsteveBlanch我使用您的方式对PDF进行签名,对我来说是正确的,区别在于我自己生成了
CMS
签名,并且我有私钥(因此我不会将
null
作为
appearance.setCrypto
调用的第一个参数传递),我认为PDF中包含了
privateKey
中的一些信息,这也是执行验证所必需的。因此,由于生成哈希时没有此信息,因此可能存在错误。我不知道是否有可能生成一个有效的签名