我正在尝试用Java验证数字签名的PDF文档。
我正在使用Apache PDFBox 2.0.6获取签名和签名的原始PDF,然后我使用Bouncy Castle来验证分离的签名(计算原始文件的哈希值,使用签名者的公共验证签名)键并比较结果)。
我读了this article并尝试使用此代码获取签名字节和原始PDF字节:
PDDocument doc = PDDocument.load(signedPDF);
byte[] origPDF = doc.getSignatureDictionaries().get(0).getSignedContent(signedPDF);
byte[] signature = doc.getSignatureDictionaries().get(0).getContents(signedPDF);
但是,当我将origPDF保存到文件时,我注意到它仍然具有签名字段,原始PDF已签名没有。此外,保存origPDF的大小为21 kb,而原始PDF的大小为15 kb。这可能是因为签名字段。
但是,当我尝试从origPDF中删除签名字段时:
public byte[] stripCryptoSig(byte[] signedPDF) throws IOException {
PDDocument pdDoc = PDDocument.load(signedPDF);
PDDocumentCatalog catalog = pdDoc.getDocumentCatalog();
PDAcroForm form = catalog.getAcroForm();
List<PDField> acroFormFields = form.getFields();
for (PDField field: acroFormFields) {
if (field.getFieldType().equalsIgnoreCase("Sig")) {
System.out.println("START removing Sign Flags");
field.setReadOnly(true);
field.setRequired(false);
field.setNoExport(true);
System.out.println("END removing Sign Flags");
/*System.out.println("START flattenning field");
field.getAcroForm().flatten();
field.getAcroForm().refreshAppearances();
System.out.println("END flattenning field");
*/
field.getAcroForm().refreshAppearances();
}
}
我得到以下的保护:
警告:字典无效,找到:'['但预期:'/'在偏移15756
警告:尚未实现签名字段的外观生成 - 您需要手动生成/更新
而且,当我在Acrobat中打开PDF时,签名字段消失了,但我看到签名的图像,其中签名曾经是PDF页面的一部分。这很奇怪,因为我认为我使用byte [] origPDF = doc.getSignatureDictionaries().get(0).getSignedContent(signedPDF);
顺便说一句,我在origPDF上调用了stripCryptoSig(byte [] signedPDF)函数,所以这不是错误。
当我尝试使用充气城堡验证签名时,我收到一条消息的异常:消息 - 摘要属性值与计算值不匹配
我想这是因为签名的原始PDF和我使用doc.getSignatureDictionaries().get(0).getSignedContent(signedPDF);
从PDFBox获得的PDF不一样。
这是我的充气城堡验证码:
private SignatureInfo verifySig(byte[] signedData, boolean attached) throws OperatorCreationException, CertificateException, CMSException, IOException {
SignatureInfo signatureInfo = new SignatureInfo();
CMSSignedData cmsSignedData;
if (attached) {
cmsSignedData = new CMSSignedData(signedData);
}
else {
PDFUtils pdfUtils = new PDFUtils();
pdfUtils.init(signedData);
signedData = pdfUtils.getSignature(signedData);
byte[] sig = pdfUtils.getSignedContent(signedData);
cmsSignedData = new CMSSignedData(new CMSProcessableByteArray(signedData), sig);
}
SignerInformationStore sis = cmsSignedData.getSignerInfos();
Collection signers = sis.getSigners();
Store certStore = cmsSignedData.getCertificates();
Iterator it = signers.iterator();
signatureInfo.setValid(false);
while (it.hasNext()) {
SignerInformation signer = (SignerInformation) it.next();
Collection certCollection = certStore.getMatches(signer.getSID());
Iterator certIt = certCollection.iterator();
X509CertificateHolder cert = (X509CertificateHolder) certIt.next();
if(signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert))){
signatureInfo.setValid(true);
if (attached) {
CMSProcessableByteArray userData = (CMSProcessableByteArray) cmsSignedData.getSignedContent();
signatureInfo.setSignedDoc((byte[]) userData.getContent());
}
else {
signatureInfo.setSignedDoc(signedData);
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String signedOnDate = "null";
String validFromDate = "null";
String validToDate = "null";
Date signedOn = this.getSignatureDate(signer);
Date validFrom = cert.getNotBefore();
Date validTo = cert.getNotAfter();
if(signedOn != null) {
signedOnDate = sdf.format(signedOn);
}
if(validFrom != null) {
validFromDate = sdf.format(validFrom);
}
if(validTo != null) {
validToDate = sdf.format(validTo);
}
DefaultAlgorithmNameFinder algNameFinder = new DefaultAlgorithmNameFinder();
signatureInfo.setSignedBy(IETFUtils.valueToString(cert.getSubject().getRDNs(BCStyle.CN)[0].getFirst().getValue()));
signatureInfo.setSignedOn(signedOn);
signatureInfo.setIssuer(IETFUtils.valueToString(cert.getIssuer().getRDNs(BCStyle.CN)[0].getFirst().getValue()));
signatureInfo.setValidFrom(validFrom);
signatureInfo.setValidTo(validTo);
signatureInfo.setVersion(String.valueOf(cert.getVersion()));
signatureInfo.setSignatureAlg(algNameFinder.getAlgorithmName(signer.getDigestAlgorithmID()) + " WTIH " + algNameFinder.getAlgorithmName(cert.getSubjectPublicKeyInfo().getAlgorithmId()));
/*signatureInfo.put("Signed by", IETFUtils.valueToString(cert.getSubject().getRDNs(BCStyle.CN)[0].getFirst().getValue()));
signatureInfo.put("Signed on", signedOnDate);
signatureInfo.put("Issuer", IETFUtils.valueToString(cert.getIssuer().getRDNs(BCStyle.CN)[0].getFirst().getValue()));
signatureInfo.put("Valid from", validFromDate);
signatureInfo.put("Valid to", validToDate);
signatureInfo.put("Version", "V" + String.valueOf(cert.getVersion()));
signatureInfo.put("Signature algorithm", algNameFinder.getAlgorithmName(signer.getDigestAlgorithmID()) + " WTIH " + algNameFinder.getAlgorithmName(cert.getSubjectPublicKeyInfo().getAlgorithmId()));*/
break;
}
}
return signatureInfo;
}
答案 0 :(得分:2)
您似乎对getSignedContent
方法以及一般的PDF签名存在误解。
我使用Apache PDFBox 2.0.6获取签名和签名的原始PDF
如果通过&#34;签署的原始PDF&#34; 表示PDF在进入签名过程之前,那么通用签名PDF的任务的第二部分是不可能的。
原因是创建实际签名之前的原始PDF是为签名行为准备的。
这个准备工作可能意味着为预先存在的空签名字段添加值字典(包括稍后注入签名容器的间隙)作为增量更新,使原始PDF保持原始签名的原始PDF文档。
另一方面,另一方面,它可能另外意味着还会发生以下一些变化:
如果之前未签署文档,则无需将这些添加内容添加为增量更新,而是可以重新排序所有对象(已更改或未更改),重新编号,间接对象可能成为直接对象,反之亦然,未使用的对象可能会被删除,重复的对象可能会被缩减为单个对象,表单字段的字体变为只读可能会缩减为实际使用的字形,等等。
仅对于准备好的PDF,创建实际签名并将其嵌入签名值字典中留下的间隙中。
如果您应用通话
byte[] origPDF = doc.getSignatureDictionaries().get(0).getSignedContent(signedPDF);
byte[] signature = doc.getSignatureDictionaries().get(0).getContents(signedPDF);
到签名文档,origPDF
包含签名文档的字节,但签名值字典中的间隙除外,signature
包含间隙的(十六进制解码)内容。
因此origPDF
特别包含准备期间所做的所有更改;因此,称之为orig
会引起误导。
此外,由于缺少最初为签名容器保留的间隙,这些字节很可能实际上不再形成有效的PDF:PDF包含指向起始偏移的交叉引用(从头开始)每个PDF对象的文档);由于差距已经消失,其前一个位置移动后的字节数和偏移量现在都是错误的。
因此,您的origPDF
仅包含有符号字节的集合,这可能与您认为原始文件的文件非常不同。
您的verifySig
完全忽略了签名字段值字典的 SubFilter 。根据该值,使用getContents
检索的签名字节可能具有完全不同的内容。
因此,如果没有您签名的PDF,进一步审查该方法是没有意义的。
答案 1 :(得分:1)
在我的情况下,我在设置签名和signedData的代码中出错。我不小心把这些价值换了。
所以,而不是:
signedData = pdfUtils.getSignature(signedData);
byte[] sig = pdfUtils.getSignedContent(signedData);
应该是:
byte[] sig = pdfUtils.getSignature(signedData);
signedData = pdfUtils.getSignedContent(signedData);
现在,它正在运作。我用它测试的文件是使用adbe.pkcs7.detached
签名的。但是,如果使用其他签名方法,它就不会起作用。
所以,感谢@Tilman Hausherr指出我的ShowSignature.java示例。 这就是签名验证的方式。
并且还要感谢@mkl的详细解释。
我现在明白,当创建签名时,会添加签名字段,并根据该新值计算哈希值。这就是验证工作的原因。您不需要没有签名字段的原始PDF。