在他的更新中,OP几乎正确,只有两个错误:
- 他尝试
InputStream
两次读取参数内容:
CMSTypedData msg = new CMSProcessableByteArray(IOUtils.toByteArray(content)); [...] Attribute attr = new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(md.digest(IOUtils.toByteArray(content)))));
因此,在第二次尝试之前已经从流中读取了所有数据,因此返回了empty
byte[]。因此,消息摘要属性包含错误的哈希值。
- 他以复杂的方式创建了最终的CMS容器:
return new CMSSignedData(msg, s.getEnpred()).getEnpred();
将后者减少到实际需要的数量,事实证明不再需要
CMSTypedData msg。因此,前者被隐式解决。
在将摘要计算重新安排到方法的顶部并另外切换到SHA256后(由于在许多情况下不赞成使用SHA1,我更喜欢使用其他哈希算法)并允许使用证书
chain而不是单个证书
certificate,该方法看起来像这个:
// Digest generation step MessageDigest md = MessageDigest.getInstance("SHA256", "BC"); byte[] digest = md.digest(IOUtils.toByteArray(content)); // Separate signature container creation step List<Certificate> certList = Arrays.asList(chain); JcaCertStore certs = new JcaCertStore(certList); CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); Attribute attr = new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(digest))); ASN1EncodableVector v = new ASN1EncodableVector(); v.add(attr); SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider()) .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v))); AlgorithmIdentifier sha256withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA"); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); InputStream in = new ByteArrayInputStream(chain[0].getEnpred()); X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in); gen.addSignerInfoGenerator(builder.build( new BcRSAContentSignerBuilder(sha256withRSA, new DefaultDigestAlgorithmIdentifierFinder().find(sha256withRSA)) .build(PrivateKeyFactory.createKey(pk.getEnpred())), new JcaX509CertificateHolder(cert))); gen.addCertificates(certs); CMSSignedData s = gen.generate(new CMSAbsentContent(), false); return s.getEnpred();( CreateSignature方法signWithSeparatedHashing
)
用在相当少的签名代码框架中
void sign(PDdocument document, OutputStream output, SignatureInterface signatureInterface) throws IOException { PDSignature signature = new PDSignature(); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); signature.setName("Example User"); signature.setLocation("Los Angeles, CA"); signature.setReason("Testing"); signature.setSignDate(Calendar.getInstance()); document.addSignature(signature); ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output); byte[] cmsSignature = signatureInterface.sign(externalSigning.getContent()); externalSigning.setSignature(cmsSignature); }( CreateSignature方法sign
)
像这样
try ( InputStream resource = getClass().getResourceAsStream("test.pdf"); OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "testSignedWithSeparatedHashing.pdf")); PDdocument pddocument = PDdocument.load(resource) ) { sign(pddocument, result, data -> signWithSeparatedHashing(data)); }( CreateSignature测试方法testSignWithSeparatedHashing
)
导致正确签名的PDF,至少与所讨论的证书和私钥适合当前任务一样适当。
一句话:
使用的OP
IOUtils.toByteArray(content))(我在上面的代码中也是如此)。但是考虑到OP的开场白
如果PDF文件太大而无法上传怎么办?例如:100mb
这样做并不是一个好主意,因为它一次仅将一个大文件加载到内存中以进行哈希处理。如果确实要考虑一个应用程序的资源占用量,则应一次读取该流几个KB,然后连续使用来消化数据,
MessageDigest.update并且仅
MessageDigest.digest在最后使用来获得结果哈希值。



