PDF外部签名插入SignedHash验证失败问题及解决方案
PDF数字签名后提示文件被修改的解决方案
我最近在实现客户端持有待签名PDF+公钥证书,外部硬件存储对应私钥并返回签名哈希的场景时,踩了个大坑:签名完成后用Adobe Reader打开,始终提示"文件在签名后被修改",试过切换可见/不可见签名、调整时间戳等方法都没用,最终在帮助下找到了核心问题的解决办法。
问题场景与现象
需求场景
- 客户端拥有待签名PDF和对应的公钥证书
- 私钥存储在外部硬件设备中,需要将哈希值发送给硬件,由硬件返回SignedHash
- 用硬件返回的SignedHash完成PDF签名流程
异常现象
签名生成后,Adobe Reader识别到签名,但判定文件已被修改,签名无法通过验证。
已修复的完整实现代码
package com.Marloo; import org.apache.commons.codec.Charsets; import org.bouncycastle.util.encoders.Base64; import com.itextpdf.text.DocumentException; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.*; import com.itextpdf.text.pdf.security.*; import java.io.*; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.*; public class Test { public static final String CERT = "src/main/resources/certificate.pem"; public static final String SRC = "src/main/resources/tmp.pdf"; public static final String DEST = "src/main/resources/signed.pdf"; public static void main(String args[]) throws IOException { getHash(SRC, CERT); } public static void getHash(String doc, String cert) throws IOException { try { File initialFile = new File(cert); InputStream is = new FileInputStream(initialFile); // 获取客户端的自签名证书 CertificateFactory factory = CertificateFactory.getInstance("X.509"); Certificate[] chain = new Certificate[1]; chain[0] = factory.generateCertificate(is); // 创建PDF阅读器和签名Stamper PdfReader reader = new PdfReader(doc); ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0'); // 设置签名外观 PdfSignatureAppearance sap = stamper.getSignatureAppearance(); sap.setReason("TEST REASON"); sap.setLocation("TEST LOCATION"); //sap.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, "sig"); //可见签名 sap.setVisibleSignature(new Rectangle(36, 748, 36, 748), 1, "sig"); //不可见签名 sap.setCertificate(chain[0]); // 创建签名字典 PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED); dic.setReason(sap.getReason()); dic.setLocation(sap.getLocation()); dic.setContact(sap.getContact()); dic.setDate(new PdfDate(sap.getSignDate())); sap.setCryptoDictionary(dic); HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>(); exc.put(PdfName.CONTENTS, new Integer(8192 * 2 + 2)); sap.preClose(exc); ExternalDigest externalDigest = new ExternalDigest() { public MessageDigest getMessageDigest(String hashAlgorithm) throws GeneralSecurityException { return DigestAlgorithms.getMessageDigest(hashAlgorithm, null); } }; PdfPKCS7 sgn = new PdfPKCS7(null, chain, "SHA256", null, externalDigest, false); InputStream data = sap.getRangeStream(); byte hash[] = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256")); // 获取OCSP和CRL(可选,用于证书有效性验证) OCSPVerifier ocspVerifier = new OCSPVerifier(null, null); OcspClient ocspClient = new OcspClientBouncyCastle(ocspVerifier); byte[] ocsp = null; if (chain.length >= 2 && ocspClient != null) { ocsp = ocspClient.getEncoded((X509Certificate) chain[0], (X509Certificate) chain[1], null); } // 关键修复:生成PKCS#7认证属性的哈希值,而非直接使用文档哈希 byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, null, null, MakeSignature.CryptoStandard.CMS); InputStream sh_is = new ByteArrayInputStream(sh); byte[] signedAttributesHash = DigestAlgorithms.digest(sh_is, externalDigest.getMessageDigest("SHA256")); System.out.println("----------------------------------------------"); System.out.println("需发送到硬件签名的哈希(Base64格式):"); System.out.println( new String(Base64.encode(signedAttributesHash), Charsets.UTF_8)); System.out.println("----------------------------------------------"); System.out.println("请输入硬件返回的Base64格式签名哈希,按回车确认:"); System.out.println("----------------------------------------------"); Scanner in = new Scanner(System.in); String signedHashB64 = in.nextLine(); System.out.println("收到的签名哈希: " + signedHashB64); ByteArrayOutputStream os = baos; byte[] signedHash = org.apache.commons.codec.binary.Base64.decodeBase64(signedHashB64.getBytes()); // 完成PDF签名流程 sgn.setExternalDigest(signedHash, null, "RSA"); Collection<byte[]> crlBytes = null; TSAClientBouncyCastle tsaClient = new TSAClientBouncyCastle("http://timestamp.gdca.com.cn/tsa", null, null); byte[] encodedSig = sgn.getEncodedPKCS7(hash, tsaClient, ocsp, crlBytes, MakeSignature.CryptoStandard.CMS); byte[] paddedSig = new byte[8192]; System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length); PdfDictionary dic2 = new PdfDictionary(); dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true)); try { sap.close(dic2); } catch (DocumentException e) { throw new IOException(e); } FileOutputStream fos = new FileOutputStream(new File(DEST)); os.writeTo(fos); System.out.println("签名完成,文件路径: " + System.getProperty("user.dir") + "/" + DEST); System.out.println("------------------操作结束--------------------------"); System.exit(0); } catch (GeneralSecurityException e) { throw new IOException(e); } catch (DocumentException e) { throw new IOException(e); } } }
核心修复逻辑
之前的错误在于:直接将文档哈希发送给硬件签名,但对于我们使用的PKCS#7 detached签名(代码中PdfName.ADBE_PKCS7_DETACHED类型),硬件需要签名的不是原始文档哈希,而是PKCS#7包内的认证属性集合哈希值(也就是代码中的signedAttributesHash变量)。
这个认证属性集合包含了文档哈希、签名算法、签名时间戳等关键元数据,只有对这个哈希进行签名,生成的PKCS#7包结构才合法,PDF阅读器才能正确验证签名有效性。之前跳过这一步直接签名文档哈希,导致签名包结构不符合规范,阅读器就会判定文件已被修改。
内容的提问来源于stack exchange,提问作者Fabrizio Barone
相关产品推荐
相关产品推荐

