我们正试图验证荷兰政府机构(UWV Verzekeringsbericht)的数字签名,包括文件的真实性。 Adobe Acrobat Reader能够正确验证此文件。
通过小型概念验证应用程序,我们可以验证各种数字签名PDF的真实性:
import com.itextpdf.text.pdf.AcroFields;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.security.PdfPKCS7;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Security;
import java.util.ArrayList;
public class Verifier {
public static void main(String[] args) throws IOException, GeneralSecurityException {
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
new Verifier().run(args[0]);
}
private void run(String path) throws IOException, GeneralSecurityException {
final PdfReader reader = new PdfReader(path);
final AcroFields fields = reader.getAcroFields();
final ArrayList<String> signatureNames = fields.getSignatureNames();
for(String signatureName: signatureNames) {
System.out.println("Verify signature " + signatureName);
verifySignature(fields, signatureName);
}
}
private PdfPKCS7 verifySignature(final AcroFields fields, final String name) throws GeneralSecurityException {
System.out.println("Signature covers whole document: " + fields.signatureCoversWholeDocument(name));
System.out.println("Document revision: " + fields.getRevision(name) + " of " + fields.getTotalRevisions());
PdfPKCS7 pkcs7 = fields.verifySignature(name);
System.out.println("Integrity check OK? " + pkcs7.verify());
return pkcs7;
}
}
使用这些(Maven)依赖项:
<dependencies>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-debug-jdk15on</artifactId>
<version>1.60</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.60</version>
</dependency>
</dependencies>
您可以猜测,验证来自此权限的PDF是行不通的。
运行此应用程序的结果是:
Exception in thread "main" java.lang.IllegalArgumentException: can't decode PKCS7SignedData object
at com.itextpdf.text.pdf.security.PdfPKCS7.<init>(PdfPKCS7.java:214)
这是在PdfPKCS7类中引起的,该类从签名的内容实例化ASN1输入流(第203行):
SN1InputStream din = new ASN1InputStream(new ByteArrayInputStream(contentsKey));
这反过来会导致IOException:DER长度超过4个字节:31因此签名似乎无效。
AcroFields的verifySignature
方法调用尝试创建PdfPKCS7实例。此方法的代码段:
if(!reader.isEncrypted()){
pk = new PdfPKCS7(contents.getOriginalBytes(), sub, provider);
}else{
pk = new PdfPKCS7(contents.getBytes(),sub,provider);
}
出于某种原因,iTextPDF得出结论,PDF已加密,并使用getBytes
变体进行签名验证。但是,PDF没有加密(据我所知),因此它应该使用getOriginalBytes
。
当我强制使用这个原始内容时,在调试时,验证成功!
因此,它似乎是iTextPDF中的一个错误,可能是由pdf中不寻常的因素组合引起的。
PDF证书中的一些细节:
Version: 3
Signature algorithm: SHA256 RSA
Key usage: Digital Signature, Encrypt Keys
Public Key: RSA (2048 bits)
不幸的是,我不能分享有关的PDF,因为它包含个人信息。作为荷兰公民,您可以从UWV下载自己的版本,请参阅these instructions。
任何帮助或建议表示赞赏。
此问题的背景似乎是ISO 32000-1 PDF规范中缺少的信息; iText 5.5同时支持ISO 32000-1逐字解释。
在ISO 32000-2中,同时澄清了这一点。
在PDF成为ISO标准之前,PDF处理器实施者遵循Adobe Acrobat的指导,因为PDF文档的文档不清楚甚至没有声明。
当Adobe Acrobat加密并签名PDF时,包含签名容器的二进制字符串未加密。因此,在这种情况下,其他PDF工具也没有加密签名容器。
2008年,PDF成为ISO标准。根据ISO 32000-1,
加密适用于文档PDF文件中的所有字符串和流,但以下情况除外:
- 预告片中ID条目的值
- Encrypt字典中的任何字符串
- 流内部的任何字符串,例如内容流和压缩对象流,它们本身都是加密的
(ISO 32000-1,第7.6节 - 加密)
据此,在加密和签名的PDF中,包含嵌入式签名容器的二进制字符串也将被加密。
2017年,ISO 32000的第2部分发布。其中,上面的枚举由新条目扩展
- 表示签名字典中Contents键值的任何十六进制字符串
(ISO 32000-2,第7.6节 - 加密)
据此,在加密和签名的PDF中,包含嵌入式签名容器的二进制字符串不会被加密。
在iText中用于检索我可以找到的签名容器的最早代码中,假定包含签名容器的二进制字符串永远不会被加密:
pk = new PdfPKCS7(contents.getOriginalBytes(), provider);
(提交2004年11月5日的ffc70db,评为“paulo version 139”)
方法getOriginalBytes
检索PDF字符串中的字节,因为它们在PDF中,不会应用任何解密。
后来代码被移动了两三次没有变化。
添加PAdES支持时,此处仅添加了子过滤器,仍然使用了原始字节:
pk = new PdfPKCS7(contents.getOriginalBytes(), sub, provider);
(提交日期为2012年8月31日的691281c,评论为“验证CAdES签名”)
但在2017年初,它被更改为您找到的代码:
if(!reader.isEncrypted()){
pk = new PdfPKCS7(contents.getOriginalBytes(), sub, provider);
}else{
pk = new PdfPKCS7(contents.getBytes(),sub,provider);
}
(提交日期为2017年2月9日的0b852d7,评论为“验证签名SUP-1783时处理加密内容流”)
显然,支持问题SUP-1783触发了对ISO 32000-1的逐字解释的转换。
在iText 7中我们有
pk = new PdfPKCS7(PdfEncodings.convertToBytes(contents.getValue(), null), sub, provider);
(提交ae73650,日期为2015年10月11日,评论为“增加了支持LTV,Ocsp,CRL和TSA的类。”)
但这里的contents
标记为未加密
contents.markAsUnencryptedObject();
(提交日期为2018年4月24日的6dfb206,评论为“当传递只读文档时避免在SignatureUtil中出现异常”)
在iText 7中,这使得contents.getValue()
返回原始字节。所以iText 7支持PDF 2.0的澄清。
在我看来,考虑到逐字ISO 32000-1解释,人们应该接受加密或未加密的签名容器,但是根据ISO 32000-2措辞,应该只产生未加密的容器。