使用 PDFBox 2.0.15 的外部签名
External Signature with PDFBox 2.0.15
我正在实施一个应用程序来在服务器中签署 PDF 文件,具有以下场景(长历史,短历史):
- 客户端开始向服务器发送签名,date/time和水印
- 服务器添加签名字典到文件中并发送待签名数据
- 客户签到内容
- 服务器完成签名
我正在使用 PDFBox 2.0.15,并使用新功能 saveIncrementalForExternalSigning
,如下面的代码所示:
try {
String name = document.getID();
File signedFile = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
this.log("[SIGNATURE] Creating signed version of the document");
if (signedFile.exists()) {
signedFile.delete();
}
FileOutputStream tbsFos = new FileOutputStream(signedFile);
ExternalSigningSupport externalSigning = pdfdoc.saveIncrementalForExternalSigning(tbsFos);
byte[] content = readExternalSignatureContent(externalSigning);
if (postparams.get("action").equalsIgnoreCase("calc_hash")) {
this.log("[SIGNATURE] Calculating hash of the document");
String strBase64 = ParametersHandle.compressParamBase64(content);
// this saves the file with a 0 signature
externalSigning.setSignature(new byte[0]);
// remember the offset (add 1 because of "<")
int offset = signature.getByteRange()[1] + 1;
this.log("[SIGNATURE] Sending calculated hash to APP");
return new String[] { strBase64, processID, String.valueOf(offset) };
} else {
this.log("[SIGNATURE] Signature received from APP");
String signature64 = postparams.get("sign_disgest");
byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);
this.log("[SIGNATURE] Setting signature to document");
externalSigning.setSignature(cmsSignature);
pdfdoc.close();
IOUtils.closeQuietly(signatureOptions);
this.log("[DOXIS] Creating new version of document on Doxis");
createNewVersionOfDocument(doxisServer, documentServer, doxisSession, document, signedFile);
return new String[] { "SIGNOK" };
}
} catch (IOException ex) {
this.log("[SAVE FOR SIGN] " + ex);
return null;
}
在 "IF" 语句中,我正在生成要签名的数据。在 "ELSE" 语句中,通过 post 请求(ParametersHandle.decompressParamFromBase64
所做的)将签名添加到文档中。所以我在这次尝试中有两个 post 请求此方法。
第二种方法是用一种方法处理每个 post 请求,所以我有第二个代码块:
// remember the offset (add 1 because of "<")
int offset = Integer.valueOf(postparams.get("offset"));
this.log("[PDF BOX] Retrieving offset of bytes range for this signature. The value is: "
+ String.valueOf(offset));
File signedPDF = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
this.log("[SIGNATURE] Reloading document for apply signature: " + signedPDF.getAbsolutePath());
// invoke external signature service
String signature64 = postparams.get("sign_disgest");
byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);
this.log("[SIGNATURE] Got signature byte array from APP.");
// set signature bytes received from the service
// now write the signature at the correct offset without any PDFBox methods
this.log("[SIGNATURE] Writing signed document...");
RandomAccessFile raf = new RandomAccessFile(signedPDF, "rw");
raf.seek(offset);
raf.write(Hex.getBytes(cmsSignature));
raf.close();
this.log("[SIGNATURE] New signed document has been saved!");
问题是:我在 Adobe Reader 上验证时收到错误 "The document has been altered or corrupted since the Signature was applied"。
根据我的理解,这不应该发生,因为在第二次 post 调用时会记住签名字节范围的偏移量。
感谢任何帮助或想法,
提前致谢。
[编辑]
已用文件的完整列表:https://drive.google.com/drive/folders/1S9a88lCGaQYujlEyCrhyzqvmWB-68LR3
[编辑 2]
根据@mkl 的评论,这里是签名的方法:
public byte[] sign(byte[] hash)
throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "");
X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);
try
{
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
X509Certificate cert = (X509Certificate) certificateChain[0];
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert));
gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
CMSProcessableInputStream msg = new CMSProcessableInputStream(new ByteArrayInputStream(hash));
CMSSignedData signedData = gen.generate(msg, false);
return signedData.getEncoded();
}
catch (GeneralSecurityException e)
{
throw new IOException(e);
}
catch (CMSException e)
{
throw new IOException(e);
}
catch (OperatorCreationException e)
{
throw new IOException(e);
}
}
我已经测试了 CreateVisibleSignature2
示例,替换了调用此服务的 sign
方法 returns 我的签名,它有效。
感谢 Tilman Hausherr 我可以弄清楚发生了什么:
1 - 我有一个与SmatCards等通信的桌面APP,它是签名者。我们使用 WebSocket 与服务器通信(通过网页)。我已经编写了自己的 websocket 服务器 class,这就是它只准备使用 65k 字节的原因。比起我尝试在此处发送数据时:
ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
byte[] cmsSignature = sign(externalSigning.getContent());
我在应用程序中遇到错误。
2 - Tilman 建议我看一下这个 ,他做了同样的事情:创建 externalSigning.getContent()
的 SHA256 散列并发送到另一个地方进行签名。我不知道为什么,但唯一对我不起作用的是:
gen.addSignerInfoGenerator(builder.build(
new BcRSAContentSignerBuilder(sha256withRSA,
new DefaultDigestAlgorithmIdentifierFinder().find(sha256withRSA))
.build(PrivateKeyFactory.createKey(pk.getEncoded())),
new JcaX509CertificateHolder(cert)));
因此,我已将此块替换为:
ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
那么,我完整的签名方法是这样的:
PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "changeit");
X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);
List<X509Certificate> certList = Arrays.asList(certificateChain);
JcaCertStore certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Attribute attr = new Attribute(CMSAttributes.messageDigest,
new DERSet(new DEROctetString(hash)));
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(certificateChain[0].getEncoded());
X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in);
ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
gen.addSignerInfoGenerator(builder.build(sha256Signer, new JcaX509CertificateHolder(cert)));
gen.addCertificates(certs);
CMSSignedData s = gen.generate(new CMSAbsentContent(), false);
return s.getEncoded();
所以,再次感谢社区!!!
我正在实施一个应用程序来在服务器中签署 PDF 文件,具有以下场景(长历史,短历史):
- 客户端开始向服务器发送签名,date/time和水印
- 服务器添加签名字典到文件中并发送待签名数据
- 客户签到内容
- 服务器完成签名
我正在使用 PDFBox 2.0.15,并使用新功能 saveIncrementalForExternalSigning
,如下面的代码所示:
try {
String name = document.getID();
File signedFile = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
this.log("[SIGNATURE] Creating signed version of the document");
if (signedFile.exists()) {
signedFile.delete();
}
FileOutputStream tbsFos = new FileOutputStream(signedFile);
ExternalSigningSupport externalSigning = pdfdoc.saveIncrementalForExternalSigning(tbsFos);
byte[] content = readExternalSignatureContent(externalSigning);
if (postparams.get("action").equalsIgnoreCase("calc_hash")) {
this.log("[SIGNATURE] Calculating hash of the document");
String strBase64 = ParametersHandle.compressParamBase64(content);
// this saves the file with a 0 signature
externalSigning.setSignature(new byte[0]);
// remember the offset (add 1 because of "<")
int offset = signature.getByteRange()[1] + 1;
this.log("[SIGNATURE] Sending calculated hash to APP");
return new String[] { strBase64, processID, String.valueOf(offset) };
} else {
this.log("[SIGNATURE] Signature received from APP");
String signature64 = postparams.get("sign_disgest");
byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);
this.log("[SIGNATURE] Setting signature to document");
externalSigning.setSignature(cmsSignature);
pdfdoc.close();
IOUtils.closeQuietly(signatureOptions);
this.log("[DOXIS] Creating new version of document on Doxis");
createNewVersionOfDocument(doxisServer, documentServer, doxisSession, document, signedFile);
return new String[] { "SIGNOK" };
}
} catch (IOException ex) {
this.log("[SAVE FOR SIGN] " + ex);
return null;
}
在 "IF" 语句中,我正在生成要签名的数据。在 "ELSE" 语句中,通过 post 请求(ParametersHandle.decompressParamFromBase64
所做的)将签名添加到文档中。所以我在这次尝试中有两个 post 请求此方法。
第二种方法是用一种方法处理每个 post 请求,所以我有第二个代码块:
// remember the offset (add 1 because of "<")
int offset = Integer.valueOf(postparams.get("offset"));
this.log("[PDF BOX] Retrieving offset of bytes range for this signature. The value is: "
+ String.valueOf(offset));
File signedPDF = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
this.log("[SIGNATURE] Reloading document for apply signature: " + signedPDF.getAbsolutePath());
// invoke external signature service
String signature64 = postparams.get("sign_disgest");
byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);
this.log("[SIGNATURE] Got signature byte array from APP.");
// set signature bytes received from the service
// now write the signature at the correct offset without any PDFBox methods
this.log("[SIGNATURE] Writing signed document...");
RandomAccessFile raf = new RandomAccessFile(signedPDF, "rw");
raf.seek(offset);
raf.write(Hex.getBytes(cmsSignature));
raf.close();
this.log("[SIGNATURE] New signed document has been saved!");
问题是:我在 Adobe Reader 上验证时收到错误 "The document has been altered or corrupted since the Signature was applied"。 根据我的理解,这不应该发生,因为在第二次 post 调用时会记住签名字节范围的偏移量。
感谢任何帮助或想法,
提前致谢。
[编辑]
已用文件的完整列表:https://drive.google.com/drive/folders/1S9a88lCGaQYujlEyCrhyzqvmWB-68LR3
[编辑 2]
根据@mkl 的评论,这里是签名的方法:
public byte[] sign(byte[] hash)
throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "");
X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);
try
{
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
X509Certificate cert = (X509Certificate) certificateChain[0];
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert));
gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
CMSProcessableInputStream msg = new CMSProcessableInputStream(new ByteArrayInputStream(hash));
CMSSignedData signedData = gen.generate(msg, false);
return signedData.getEncoded();
}
catch (GeneralSecurityException e)
{
throw new IOException(e);
}
catch (CMSException e)
{
throw new IOException(e);
}
catch (OperatorCreationException e)
{
throw new IOException(e);
}
}
我已经测试了 CreateVisibleSignature2
示例,替换了调用此服务的 sign
方法 returns 我的签名,它有效。
感谢 Tilman Hausherr 我可以弄清楚发生了什么:
1 - 我有一个与SmatCards等通信的桌面APP,它是签名者。我们使用 WebSocket 与服务器通信(通过网页)。我已经编写了自己的 websocket 服务器 class,这就是它只准备使用 65k 字节的原因。比起我尝试在此处发送数据时:
ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
byte[] cmsSignature = sign(externalSigning.getContent());
我在应用程序中遇到错误。
2 - Tilman 建议我看一下这个 externalSigning.getContent()
的 SHA256 散列并发送到另一个地方进行签名。我不知道为什么,但唯一对我不起作用的是:
gen.addSignerInfoGenerator(builder.build(
new BcRSAContentSignerBuilder(sha256withRSA,
new DefaultDigestAlgorithmIdentifierFinder().find(sha256withRSA))
.build(PrivateKeyFactory.createKey(pk.getEncoded())),
new JcaX509CertificateHolder(cert)));
因此,我已将此块替换为:
ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
那么,我完整的签名方法是这样的:
PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "changeit");
X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);
List<X509Certificate> certList = Arrays.asList(certificateChain);
JcaCertStore certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Attribute attr = new Attribute(CMSAttributes.messageDigest,
new DERSet(new DEROctetString(hash)));
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(certificateChain[0].getEncoded());
X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in);
ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
gen.addSignerInfoGenerator(builder.build(sha256Signer, new JcaX509CertificateHolder(cert)));
gen.addCertificates(certs);
CMSSignedData s = gen.generate(new CMSAbsentContent(), false);
return s.getEncoded();
所以,再次感谢社区!!!