我有这样的场景。
我有一个生成PDF的应用程序,需要签名。
我们没有证书来签署文档,因为它们在HSM中,我们使用证书的唯一方法是使用Web服务。
这项网络服务提供了两种选择,发送PDF文档,然后返回一个签名的PDF,或者发送一个将被签名的哈希。
第一个选项是不可行的,因为PDF是在没有时间戳的情况下签名的(这是一个非常重要的必要条件),所以选择了第二个选项。
这是我们的代码,首先,我们得到签名外观,并计算哈希:
PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();
PdfStamper stamper = PdfStamper.createSignature(reader, baos, ' ', 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表示,"文档自签署以来已被更改或损坏"。
我认为我们在这个过程中犯了一些错误,我们不知道到底会是什么
我们感谢在这方面的帮助,或者其他方式。
谢谢。
编辑
正如mkl所建议的那样,我已经遵循了这本书的4.3.3节PDF文档的数字签名,现在我的代码如下:
第一部分,当我们计算哈希时:
PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();
PdfStamper stamper = PdfStamper.createSignature(reader, baos, ' ');
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服务仅返回一个签名的哈希
在这一点上,我们有了文档的哈希代码。然后我们将散列发送到Web服务,并获得签名的散列代码。
最后,我们将签名后的散列放入PDF:
如果Web服务仅返回一个签名的哈希,那么您的PDF签名是不正确的:您将签名SubFilter设置为adbe.pkcs7.destriated。这意味着签名内容必须包含一个完整的PKCS#7签名容器,而不仅仅是一个签名哈希。
您可能想下载PDF文档的数字签名,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进行了哈希)等细节造成的。
一个可能的问题:BER编码
iText从OP提供的第一个样本中生成的CMS容器中嵌入的web服务中的CMS签名容器暗示了一个可能的问题:查看上面的ASN.1转储,嵌入CMS容器中的外部结构的大小通常是NDEF
。
这表明这些外部结构是使用不太严格的BER(基本编码规则)而不是更严格的DER(可分辨编码规则)创建的,因为在DER中禁止在不说明其大小的情况下启动结构的BER选项。
PDF规范中引用的CMS规范(RFC 3852)允许对容器的外部结构进行任何BER编码,另一方面,PDF规范要求:
Contents的值应为DER编码的PKCS#7二进制数据包含签名的对象。PKCS#7对象应符合RFC3852加密消息语法。
因此,严格来说,嵌入PDF中的签名容器需要进行DER编码。
据我所知,只要签名容器DER对某些关键元素进行编码,就没有PDF签名验证器拒绝这样的签名。不过,就未来的工具而言,这种签名可能是一个失败点。
经过多次调试,我们终于发现了问题。
出于某种神秘的原因,生成文档哈希的方法被执行了两次,使第一个哈希(我们用来发送给服务)无效。
在对代码进行重构之后,原始代码工作正常。
非常感谢所有帮助我的人,尤其是mkl。