使用PDFBox+HSM签名PDF后Adobe提示签名无效(文档已篡改)的解决方法
解决PDFBox结合HSM外部签名后Adobe Reader提示签名无效的问题
核心问题分析
你的代码存在三个关键错误,导致Adobe验证签名时判定文档被篡改:
重复读取输入流导致数据丢失
在sign方法中,你两次调用content.readAllBytes():第一次传给HSM签名,第二次用于构造签名属性。InputStream只能读取一次,第二次读取会得到空字节数组,导致签名属性中的消息摘要完全错误,Adobe验证时自然不匹配。CMS签名结构构造逻辑错误
你错误地将HSM返回的签名值作为CMS的待处理内容,同时还尝试用本地导出的私钥重新签名——这完全不符合外部签名的逻辑。外部签名中,HSM已经对文档哈希完成签名,你需要做的是把这个签名值嵌入到CMS结构的SignerInfo中,而不是重新签名。签名属性OID使用错误
构造签名属性时,你使用了NISTObjectIdentifiers.id_sha256作为属性OID,正确的消息摘要属性OID应该是PKCSObjectIdentifiers.id_messageDigest,属性值是文档的SHA256哈希值。
修复后的代码
public void signPdfExternally() throws Exception { PDDocument document = null; File pdfToBeSigned; File signedPdfFile; FileOutputStream output = null; byte[] cmsSignature; ExternalSigningSupport externalSigningSupport; try { pdfToBeSigned = new File("testPdf.pdf"); signedPdfFile = new File(pdfToBeSigned.getParent(), pdfToBeSigned.getName().substring(0, pdfToBeSigned.getName().lastIndexOf('.')) + "_signed.pdf"); document = Loader.loadPDF(pdfToBeSigned); output = new FileOutputStream(signedPdfFile); loadExternalCert(eSignCertProvider.getCertificateChain()); document.addSignature(signature()); externalSigningSupport = document.saveIncrementalForExternalSigning(output); // 提前读取待签名内容到字节数组,避免重复读取流 byte[] contentBytes = IOUtils.toByteArray(externalSigningSupport.getContent()); cmsSignature = sign(contentBytes); externalSigningSupport.setSignature(cmsSignature); } catch (Exception ex) { ex.printStackTrace(); throw ex; } finally { if (output != null) { try { output.close(); } catch (IOException e) { e.printStackTrace(); } } if (document != null) { document.close(); } } } private PDSignature signature() { PDSignature signature = new PDSignature(); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); signature.setName("Test User"); signature.setLocation("Some location"); signature.setReason("Testing"); signature.setSignDate(Calendar.getInstance()); // 指定签名算法,与HSM使用的算法保持一致 signature.setSignatureAlgorithm(COSName.getPDFName("SHA256-RSA")); return signature; } // 签名方法:接收待签名的字节数组,构造正确的CMS签名 public byte[] sign(byte[] contentBytes) throws IOException { try { Security.addProvider(new BouncyCastleProvider()); X509Certificate[] certChain = eSignCertProvider.getCertificateChain(); if (certChain == null || certChain.length == 0) { throw new IllegalArgumentException("No certificate chain available"); } // 计算文档SHA256哈希(如果HSM要求直接传入文档字节而非哈希,可直接传contentBytes给signContent) byte[] documentHash = MessageDigest.getInstance("SHA-256").digest(contentBytes); // 调用HSM对文档哈希签名 byte[] hsmSignature = HsmServiceProvider.signContent(documentHash, "Testing1234"); // 构造CMS签名结构 CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); // 添加证书链到CMS List<Certificate> certList = Arrays.asList(certChain); JcaCertStore certs = new JcaCertStore(certList); gen.addCertificates(certs); // 构造签名属性:消息摘要+签名时间 ASN1EncodableVector signedAttrs = new ASN1EncodableVector(); signedAttrs.add(new Attribute(PKCSObjectIdentifiers.id_messageDigest, new DERSet(new DEROctetString(documentHash)))); signedAttrs.add(new Attribute(PKCSObjectIdentifiers.id_signingTime, new DERSet(new DERUTCTime(new Date())))); // 构造SignerInfo,直接使用HSM返回的签名值 X509Certificate signingCert = certChain[0]; JcaX509CertificateHolder certHolder = new JcaX509CertificateHolder(signingCert); AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA"); AlgorithmIdentifier digestAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); // 使用NullContentSigner嵌入HSM签名,避免本地重新签名 ContentSigner nullSigner = new ContentSigner() { @Override public AlgorithmIdentifier getAlgorithmIdentifier() { return sigAlgId; } @Override public OutputStream getOutputStream() { return new ByteArrayOutputStream(); } @Override public byte[] getSignature() { return hsmSignature; } }; SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider()) .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(signedAttrs))); SignerInfoGenerator signerInfoGen = builder.build(nullSigner, certHolder); gen.addSignerInfoGenerator(signerInfoGen); // 生成分离式CMS签名(传入空内容,因为内容不在CMS中) CMSSignedData signedData = gen.generate(new CMSProcessableByteArray(new byte[0]), true); return signedData.getEncoded(); } catch (Exception e) { e.printStackTrace(); throw new IOException(e); } }
关键修复点说明
- 避免重复读取流:提前将待签名内容读取到字节数组,确保后续所有操作使用的是完整数据。
- 修正CMS构造逻辑:使用
NullContentSigner直接嵌入HSM返回的签名值,不再尝试本地签名,完全符合外部签名流程(私钥保留在HSM中)。 - 修复签名属性:使用正确的消息摘要属性OID,传入文档的SHA256哈希值,同时添加签名时间属性提升Adobe兼容性。
- 移除本地私钥依赖:删除
extractPrivateKey()调用,外部签名不需要导出HSM中的私钥。
内容的提问来源于stack exchange,提问作者Roshan Pal
相关产品推荐
相关产品推荐

