使用 PDFBox 和 BouncyCastle 签署 PDF
Signing PDF with PDFBox and BouncyCastle
我正在尝试使用 PDFBox 签署 PDF,它确实签署了,但是当我在 adobe 中打开文档时 reader 我收到以下消息 "Document has been altered or corrupted since it was signed" 有人可以帮我找到问题。
密钥库是使用 "keytool -genkeypair -storepass 123456 -storetype pkcs12 -alias test -validity 365 -v -keyalg RSA -keystore keystore.p12"
创建的
使用 pdfbox-1.8.9 和 bcpkix-jdk15on-1.52
这是我的代码:
import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.exceptions.SignatureException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;
import java.io.*;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Calendar;
import java.util.Collections;
import java.util.Enumeration;
public class CreateSignature implements SignatureInterface {
private static PrivateKey privateKey;
private static Certificate certificate;
boolean signPdf(File pdfFile, File signedPdfFile) {
try (
FileInputStream fis1 = new FileInputStream(pdfFile);
FileInputStream fis = new FileInputStream(pdfFile);
FileOutputStream fos = new FileOutputStream(signedPdfFile);
PDDocument doc = PDDocument.load(pdfFile)) {
int readCount;
byte[] buffer = new byte[8 * 1024];
while ((readCount = fis1.read(buffer)) != -1) {
fos.write(buffer, 0, readCount);
}
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("NAME");
signature.setLocation("LOCATION");
signature.setReason("REASON");
signature.setSignDate(Calendar.getInstance());
doc.addSignature(signature, this);
doc.saveIncremental(fis, fos);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public byte[] sign(InputStream is) throws SignatureException, IOException {
try {
BouncyCastleProvider BC = new BouncyCastleProvider();
Store certStore = new JcaCertStore(Collections.singletonList(certificate));
CMSTypedDataInputStream input = new CMSTypedDataInputStream(is);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner sha512Signer = new JcaContentSignerBuilder("SHA256WithRSA").setProvider(BC).build(privateKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider(BC).build()).build(sha512Signer, new X509CertificateHolder(certificate.getEncoded())
));
gen.addCertificates(certStore);
CMSSignedData signedData = gen.generate(input, false);
return signedData.getEncoded();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) throws IOException, GeneralSecurityException, SignatureException, COSVisitorException {
char[] password = "123456".toCharArray();
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(new FileInputStream("/home/user/Desktop/keystore.p12"), password);
Enumeration<String> aliases = keystore.aliases();
String alias;
if (aliases.hasMoreElements()) {
alias = aliases.nextElement();
} else {
throw new KeyStoreException("Keystore is empty");
}
privateKey = (PrivateKey) keystore.getKey(alias, password);
Certificate[] certificateChain = keystore.getCertificateChain(alias);
certificate = certificateChain[0];
File inFile = new File("/home/user/Desktop/sign.pdf");
File outFile = new File("/home/user/Desktop/sign_signed.pdf");
new CreateSignature().signPdf(inFile, outFile);
}
}
class CMSTypedDataInputStream implements CMSTypedData {
InputStream in;
public CMSTypedDataInputStream(InputStream is) {
in = is;
}
@Override
public ASN1ObjectIdentifier getContentType() {
return PKCSObjectIdentifiers.data;
}
@Override
public Object getContent() {
return in;
}
@Override
public void write(OutputStream out) throws IOException,
CMSException {
byte[] buffer = new byte[8 * 1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
}
}
修复“文档已被更改或损坏”
错误是你用 InputStream
调用 PDDocument.saveIncremental
只是覆盖原始 PDF:
FileInputStream fis1 = new FileInputStream(pdfFile);
FileInputStream fis = new FileInputStream(pdfFile);
FileOutputStream fos = new FileOutputStream(signedPdfFile);
...
doc.saveIncremental(fis, fos);
但该方法要求 InputStream
涵盖原始文件以及为准备签名所做的更改。
因此,fis
也需要指向signedPdfFile
,由于该文件之前可能不存在,因此必须调换fis
和fos
的创建顺序>
FileInputStream fis1 = new FileInputStream(pdfFile);
FileOutputStream fos = new FileOutputStream(signedPdfFile);
FileInputStream fis = new FileInputStream(signedPdfFile);
...
doc.saveIncremental(fis, fos);
不幸的是,JavaDocs 没有指出这一点。
另一个问题
生成的签名还有一个问题。如果您查看示例结果的 ASN.1 转储,您会看到类似这样的内容:
<30 80>
0 NDEF: SEQUENCE {
<06 09>
2 9: OBJECT IDENTIFIER signedData (1 2 840 113549 1 7 2)
: (PKCS #7)
<A0 80>
13 NDEF: [0] {
<30 80>
15 NDEF: SEQUENCE {
<02 01>
17 1: INTEGER 1
<31 0F>
20 15: SET {
NDEF
长度指示表明 不定长度方法 用于对签名容器的这些外层进行编码。 基本编码规则 (BER) 允许使用此方法,但更严格的 可分辨编码规则 (DER) 不允许使用此方法。虽然通用 PKCS#7/CMS 签名允许在外层使用 BER,但 PDF 规范明确要求:
When PKCS#7 signatures are used, the value of Contents shall be a DER-encoded PKCS#7 binary data object containing the signature.
(section 12.8.3.3.1 "PKCS#7 Signatures as used in ISO 32000" / "General" in ISO 32000-1)
因此,严格来说,您的签名在结构上什至是无效的。不过,通常情况下,这不会被 PDF 签名验证服务检测到,因为它们中的大多数使用标准 PKCS#7/CMS 库或方法来验证签名容器。
如果您想确保您的签名是真正有效的 PDF 签名,您可以通过替换
来实现
return signedData.getEncoded();
类似
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DEROutputStream dos = new DEROutputStream(baos);
dos.writeObject(signedData.toASN1Structure());
return baos.toByteArray();
现在整个签名对象都是 DER 编码的。
(您可以在此处找到使用原始代码和固定代码创建签名的测试,有或没有改进的编码:SignLikeLoneWolf.java)
我正在尝试使用 PDFBox 签署 PDF,它确实签署了,但是当我在 adobe 中打开文档时 reader 我收到以下消息 "Document has been altered or corrupted since it was signed" 有人可以帮我找到问题。
密钥库是使用 "keytool -genkeypair -storepass 123456 -storetype pkcs12 -alias test -validity 365 -v -keyalg RSA -keystore keystore.p12"
创建的使用 pdfbox-1.8.9 和 bcpkix-jdk15on-1.52
这是我的代码:
import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.exceptions.SignatureException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;
import java.io.*;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Calendar;
import java.util.Collections;
import java.util.Enumeration;
public class CreateSignature implements SignatureInterface {
private static PrivateKey privateKey;
private static Certificate certificate;
boolean signPdf(File pdfFile, File signedPdfFile) {
try (
FileInputStream fis1 = new FileInputStream(pdfFile);
FileInputStream fis = new FileInputStream(pdfFile);
FileOutputStream fos = new FileOutputStream(signedPdfFile);
PDDocument doc = PDDocument.load(pdfFile)) {
int readCount;
byte[] buffer = new byte[8 * 1024];
while ((readCount = fis1.read(buffer)) != -1) {
fos.write(buffer, 0, readCount);
}
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("NAME");
signature.setLocation("LOCATION");
signature.setReason("REASON");
signature.setSignDate(Calendar.getInstance());
doc.addSignature(signature, this);
doc.saveIncremental(fis, fos);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public byte[] sign(InputStream is) throws SignatureException, IOException {
try {
BouncyCastleProvider BC = new BouncyCastleProvider();
Store certStore = new JcaCertStore(Collections.singletonList(certificate));
CMSTypedDataInputStream input = new CMSTypedDataInputStream(is);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner sha512Signer = new JcaContentSignerBuilder("SHA256WithRSA").setProvider(BC).build(privateKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider(BC).build()).build(sha512Signer, new X509CertificateHolder(certificate.getEncoded())
));
gen.addCertificates(certStore);
CMSSignedData signedData = gen.generate(input, false);
return signedData.getEncoded();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) throws IOException, GeneralSecurityException, SignatureException, COSVisitorException {
char[] password = "123456".toCharArray();
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(new FileInputStream("/home/user/Desktop/keystore.p12"), password);
Enumeration<String> aliases = keystore.aliases();
String alias;
if (aliases.hasMoreElements()) {
alias = aliases.nextElement();
} else {
throw new KeyStoreException("Keystore is empty");
}
privateKey = (PrivateKey) keystore.getKey(alias, password);
Certificate[] certificateChain = keystore.getCertificateChain(alias);
certificate = certificateChain[0];
File inFile = new File("/home/user/Desktop/sign.pdf");
File outFile = new File("/home/user/Desktop/sign_signed.pdf");
new CreateSignature().signPdf(inFile, outFile);
}
}
class CMSTypedDataInputStream implements CMSTypedData {
InputStream in;
public CMSTypedDataInputStream(InputStream is) {
in = is;
}
@Override
public ASN1ObjectIdentifier getContentType() {
return PKCSObjectIdentifiers.data;
}
@Override
public Object getContent() {
return in;
}
@Override
public void write(OutputStream out) throws IOException,
CMSException {
byte[] buffer = new byte[8 * 1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
}
}
修复“文档已被更改或损坏”
错误是你用 InputStream
调用 PDDocument.saveIncremental
只是覆盖原始 PDF:
FileInputStream fis1 = new FileInputStream(pdfFile);
FileInputStream fis = new FileInputStream(pdfFile);
FileOutputStream fos = new FileOutputStream(signedPdfFile);
...
doc.saveIncremental(fis, fos);
但该方法要求 InputStream
涵盖原始文件以及为准备签名所做的更改。
因此,fis
也需要指向signedPdfFile
,由于该文件之前可能不存在,因此必须调换fis
和fos
的创建顺序>
FileInputStream fis1 = new FileInputStream(pdfFile);
FileOutputStream fos = new FileOutputStream(signedPdfFile);
FileInputStream fis = new FileInputStream(signedPdfFile);
...
doc.saveIncremental(fis, fos);
不幸的是,JavaDocs 没有指出这一点。
另一个问题
生成的签名还有一个问题。如果您查看示例结果的 ASN.1 转储,您会看到类似这样的内容:
<30 80>
0 NDEF: SEQUENCE {
<06 09>
2 9: OBJECT IDENTIFIER signedData (1 2 840 113549 1 7 2)
: (PKCS #7)
<A0 80>
13 NDEF: [0] {
<30 80>
15 NDEF: SEQUENCE {
<02 01>
17 1: INTEGER 1
<31 0F>
20 15: SET {
NDEF
长度指示表明 不定长度方法 用于对签名容器的这些外层进行编码。 基本编码规则 (BER) 允许使用此方法,但更严格的 可分辨编码规则 (DER) 不允许使用此方法。虽然通用 PKCS#7/CMS 签名允许在外层使用 BER,但 PDF 规范明确要求:
When PKCS#7 signatures are used, the value of Contents shall be a DER-encoded PKCS#7 binary data object containing the signature.
(section 12.8.3.3.1 "PKCS#7 Signatures as used in ISO 32000" / "General" in ISO 32000-1)
因此,严格来说,您的签名在结构上什至是无效的。不过,通常情况下,这不会被 PDF 签名验证服务检测到,因为它们中的大多数使用标准 PKCS#7/CMS 库或方法来验证签名容器。
如果您想确保您的签名是真正有效的 PDF 签名,您可以通过替换
来实现return signedData.getEncoded();
类似
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DEROutputStream dos = new DEROutputStream(baos);
dos.writeObject(signedData.toASN1Structure());
return baos.toByteArray();
现在整个签名对象都是 DER 编码的。
(您可以在此处找到使用原始代码和固定代码创建签名的测试,有或没有改进的编码:SignLikeLoneWolf.java)