我正在尝试使用 Hsm 服务对 pdf 进行签名,该服务接收文档哈希并使用签名的哈希进行响应。我遵循了一些示例并检查了 Stack OverFlow 中涉及一些类似问题的帖子,但我没有找到解决方案。这里保留了数据的快速流动:
当在我的消费应用程序上收到 pdf 时,我得到一个签名的 pdf,但签名无效...它显示“文档自签名以来已被更改或损坏”。我已经尝试找到问题的根源,但没有成功,我在签名方法之后没有更改文档,只进行必要的操作以从文件中获取字节以在响应中发送它。
我知道有类似的讨论,但没有一个能解决我的问题。 这是我处理签名过程的大部分代码:
PDDocument doc = PDDocument.load(request.getRequest().getFile());
float page_width = doc.getPage(page).getMediaBox().getWidth();
float page_height = doc.getPage(page).getMediaBox().getHeight();
// Calculate position for signature
float sqr_width = page_width / num_cols;
float sqr_height = page_height / num_rows;
float pos_X = page_width - sqr_width - (sqr_width * (num_cols - column));
float pos_Y = page_height - sqr_height - (sqr_height * (num_rows - row));
GetCertificatebyTotpIDOutput certificateBody= getCertificate();
PDSignature signature = new PDSignature();
signature.setFilter( PDSignature.FILTER_ADOBE_PPKLITE );
signature.setSubFilter( PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setReason( "Test" );
signature.setSignDate(Calendar.getInstance());
Rectangle2D humanRect = new Rectangle2D.Float();
humanRect.setFrame(new Point2D.Float(pos_X, pos_Y), new Dimension((int) sqr_width, (int) sqr_height));
PDRectangle rect = createSignatureRectangle(doc, humanRect);
InputStream template = createVisualSignatureTemplate( doc, page, rect, signature, certificateBody ); // Implementation defined below.
SignatureOptions options = new SignatureOptions();
options.setVisualSignature( template );
options.setPage(0);
options.setPreferredSignatureSize(9000);
doc.addSignature( signature, options );
FileOutputStream outputStream = new FileOutputStream(filePath);
ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(outputStream);
byte[] input = IOUtils.toByteArray(externalSigning.getContent());
byte[] hash = MessageDigest.getInstance("SHA-256").digest(input);
String digest = new String(Base64.getEncoder().encode(hash));
System.out.println("Before signing: " + new String(Base64.getEncoder().encode(hash)));
byte[] signedContent3 = Files.readAllBytes(Paths.get(filePath));
byte[] hash3 = MessageDigest.getInstance("SHA-256").digest(signedContent3);
System.out.println("File OS hash: " + new String(Base64.getEncoder().encode(hash3)));
SigFinalizeOutput signedDoc = signDocument(certificateBody.getCertAlias(), "test_pdf", digest, "test", totpID, totp);
String base64HashSig = signedDoc.getSignedDocsInfo().get(0).getHashSig();
byte[] cmsSignature = sign(externalSigning.getContent(), base64HashSig);
externalSigning.setSignature(cmsSignature);
及签署方法:
public byte[] sign(InputStream content, String signedHash) throws IOException {
// cannot be done private (interface)
try {
GetCertificatebyTotpIDOutput certificateBody= getCertificate();
String base64CertificateString = certificateBody.getCert_64();
// byte[] certificateBytes = Base64.getDecoder().decode(base64CertificateString);
// CertificateFactory cf = CertificateFactory.getInstance("X.509");
// X509Certificate certificate = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateBytes));
byte[] decoded = Base64.getDecoder().decode(base64CertificateString);
Collection collection = null;
try {
collection = CertificateFactory.getInstance("X.509").generateCertificates(new ByteArrayInputStream(decoded));
} catch (CertificateException e) {
}
Certificate[] certChain = new Certificate[collection.size()];
Iterator iterator = collection.iterator();
int i = 0;
while (iterator.hasNext()) {
certChain[i++] = (Certificate) iterator.next();
}
// Certificate chain is acquired at initialization
List<Certificate> certList = new ArrayList<>();
for (Certificate cert : certChain) {
certList.add(cert);
}
Store certStore= new JcaCertStore(certList);
org.bouncycastle.asn1.x509.Certificate cert = org.bouncycastle.asn1.x509.Certificate.getInstance(certChain[0].getEncoded());
X509Certificate signerCert = (X509Certificate) certChain[0];
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
gen.addCertificates(certStore);
byte[] input = IOUtils.toByteArray(content);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(input);
System.out.println("During signing: " + new String(Base64.getEncoder().encode(messageDigest.digest())));
Attribute attr = new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(messageDigest.digest())));
ASN1EncodableVector v = new ASN1EncodableVector();
v.add(attr);
ContentSigner contentSigner = new ContentSigner() {
@Override
public byte[] getSignature() {
System.out.println(String.valueOf(Base64.getDecoder().decode(signedHash)));
return Base64.getDecoder().decode(signedHash);
}
@Override
public OutputStream getOutputStream() {
return new ByteArrayOutputStream() ;
}
@Override
public AlgorithmIdentifier getAlgorithmIdentifier() {
return new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WITHDSA");
}
};
SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
.setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v)));
gen.addSignerInfoGenerator(builder.build(contentSigner, new X509CertificateHolder(cert)));
//gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(contentSigner, signerCert));
CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
CMSSignedData cmsSignedData = gen.generate(msg, true);
byte[] result =cmsSignedData.getEncoded();
return result;
} catch (GeneralSecurityException | CMSException | OperatorCreationException e) {
throw new IOException(e);
}
}
我尝试计算字节范围,但后来我看到
externalsigning.getcontent()
已经只从pdf中检索了值,而没有签名对象。
希望有人可以帮助我至少下一步要检查什么。
在要求提供由您的代码签名的示例 PDF 后,我查看了您的代码并发现了一些问题。但很可能不是全部。
MessageDigest
类你愿意
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(input);
System.out.println("During signing: " + new String(Base64.getEncoder().encode(messageDigest.digest())));
Attribute attr = new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(messageDigest.digest())));
但是如果你阅读
MessageDigest.digest()
方法的 JavaDocs,你会发现:
/**
* Completes the hash computation by performing final operations
* such as padding. The digest is reset after this call is made.
*
* @return the array of bytes for the resulting hash value.
*/
public byte[] digest() {
因此在
System.out.println("During signing: " + new String(Base64.getEncoder().encode(messageDigest.digest())))
中检索哈希值并重置messageDigest
。因此,在下一行Attribute attr = new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(messageDigest.digest())))
中,您将检索空输入的摘要!
您明确设置了签名属性生成器:
SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
.setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v)));
因此,您知道您使用了带有签名属性的 CMS 签名容器结构。但您不签署这些属性,而是签署文档数据!
因此,您必须更改代码才能签署正确的数据。