政府文件的 PDF 数字签名验证失败
PDF digital signature verification fails on government document
我们正在尝试验证荷兰政府机构 (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 class 中引起的,它正在从签名的内容(第 203 行)实例化 ASN1 输入流:
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 文档不明确甚至声明不明确时,PDF 处理器实现者效仿 Adobe Acrobat否则。
当 Adobe Acrobat 加密并签署 PDF 时,包含签名容器的二进制字符串未加密。因此,本例中的其他 PDF 工具也未加密签名容器。
2008 年 PDF 成为 ISO 标准。根据 ISO 32000-1,
Encryption applies to all strings and streams in the document's PDF file, with the following exceptions:
- The values for the ID entry in the trailer
- Any strings in an Encrypt dictionary
- Any strings that are inside streams such as content streams and compressed object streams, which themselves are encrypted
(ISO 32000-1,第 7.6 节 - 加密)
据此,在加密和签名的 PDF 中,包含 嵌入式签名容器的二进制字符串也将被加密。
2017 年,ISO 32000 第 2 部分发布。其中上面的枚举由一个新条目扩展
- Any hexadecimal strings representing the value of the Contents key in a Signature dictionary
(ISO 32000-2,第 7.6 节 - 加密)
据此,在加密和签名的 PDF 中,包含 嵌入式签名容器的二进制字符串不会被加密。
iText中获取签名容器的代码
在我能找到的最早的 iText 检索签名容器的代码中,假定包含签名容器的二进制字符串永远不会被加密:
pk = new PdfPKCS7(contents.getOriginalBytes(), provider);
(2004 年 11 月 5 日提交 ffc70db,评论为“paulo 版本 139”)
方法 getOriginalBytes
检索 PDF 字符串的字节,因为它们在 PDF 中,从未应用解密。
后来代码搬了两三遍都没改。
添加PAdES支持时,这里只添加了subfilter,仍然使用原始字节:
pk = new PdfPKCS7(contents.getOriginalBytes(), sub, provider);
(提交 691281c,日期为 2012 年 8 月 31 日,评论为“验证 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();
(提交 6dfb206,日期为 2018 年 4 月 24 日,评论为“通过只读文档时避免在 SignatureUtil 中出现异常”)
在 iText 7 中,这使得 contents.getValue()
return 成为原始字节。所以 iText 7 支持 PDF 2.0 澄清。
应该怎么办?
在我看来,考虑到逐字的 ISO 32000-1 解释,人们应该接受加密或未加密的签名容器,但根据 ISO 32000-2 的措辞,人们应该只生成未加密的。
我们正在尝试验证荷兰政府机构 (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 class 中引起的,它正在从签名的内容(第 203 行)实例化 ASN1 输入流:
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 文档不明确甚至声明不明确时,PDF 处理器实现者效仿 Adobe Acrobat否则。
当 Adobe Acrobat 加密并签署 PDF 时,包含签名容器的二进制字符串未加密。因此,本例中的其他 PDF 工具也未加密签名容器。
2008 年 PDF 成为 ISO 标准。根据 ISO 32000-1,
Encryption applies to all strings and streams in the document's PDF file, with the following exceptions:
- The values for the ID entry in the trailer
- Any strings in an Encrypt dictionary
- Any strings that are inside streams such as content streams and compressed object streams, which themselves are encrypted
(ISO 32000-1,第 7.6 节 - 加密)
据此,在加密和签名的 PDF 中,包含 嵌入式签名容器的二进制字符串也将被加密。
2017 年,ISO 32000 第 2 部分发布。其中上面的枚举由一个新条目扩展
- Any hexadecimal strings representing the value of the Contents key in a Signature dictionary
(ISO 32000-2,第 7.6 节 - 加密)
据此,在加密和签名的 PDF 中,包含 嵌入式签名容器的二进制字符串不会被加密。
iText中获取签名容器的代码
在我能找到的最早的 iText 检索签名容器的代码中,假定包含签名容器的二进制字符串永远不会被加密:
pk = new PdfPKCS7(contents.getOriginalBytes(), provider);
(2004 年 11 月 5 日提交 ffc70db,评论为“paulo 版本 139”)
方法 getOriginalBytes
检索 PDF 字符串的字节,因为它们在 PDF 中,从未应用解密。
后来代码搬了两三遍都没改。
添加PAdES支持时,这里只添加了subfilter,仍然使用原始字节:
pk = new PdfPKCS7(contents.getOriginalBytes(), sub, provider);
(提交 691281c,日期为 2012 年 8 月 31 日,评论为“验证 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();
(提交 6dfb206,日期为 2018 年 4 月 24 日,评论为“通过只读文档时避免在 SignatureUtil 中出现异常”)
在 iText 7 中,这使得 contents.getValue()
return 成为原始字节。所以 iText 7 支持 PDF 2.0 澄清。
应该怎么办?
在我看来,考虑到逐字的 ISO 32000-1 解释,人们应该接受加密或未加密的签名容器,但根据 ISO 32000-2 的措辞,人们应该只生成未加密的。