使用 Bouncy Castle 和 PDFBox 在 Java 中验证 PDF 签名

Verifying PDF Signature in Java using Bouncy Castle and PDFBox

我正在尝试验证 Java 中的数字签名 PDF 文档。

我正在使用 Apache PDFBox 2.0.6 获取签名和已签名的原始 PDF,然后我正在使用 Bouncy Castle 验证分离的签名(计算原始文件的哈希值,使用验证签名签名者的 public 密钥并比较结果)。

我阅读了 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 处的“/”

警告:签名字段的外观生成尚未实现 - 您需要generate/update手动

而且,当我在 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;

}

您似乎对特定的 getSignedContent 方法和一般的 PDF 签名有误解。

I'm using Apache PDFBox 2.0.6 to get the signature and the original PDF that was signed

如果 "the original PDF that was signed" 你的意思是 PDF 在进入签名过程之前,那么你的任务的第二部分对于通用签名的 PDF 是不可能的。

原因是创建实际签名之前的原始 PDF 是为签名行为准备的。

这种准备可能意味着为预先存在的空签名字段添加一个值字典(包括用于稍后注入签名容器的间隙)作为增量更新,使原始 PDF 成为未触及的起始部分生成的签名文档。

不过,另一方面,这可能还意味着还会发生以下一些变化:

  • 可以从头开始创建新的签名字段;
  • 可能会在文档中添加一个额外的页面用于签名可视化;
  • 可以将额外的签名可视化(非活动图像或实际签名表单字段小部件)添加到每个页面;
  • 可能会创建表单字段的缺失外观;
  • 签名应用程序可以将其名称添加到元数据条目中作为文档处理器,最后更改的日期和时间可以更新为签名时间;
  • 在预先存在的空签名字段的情况下,由该字段的字段锁定字典指示的表单字段可以设置为只读;
  • 等pp

如果文档之前没有签名,这些添加不需要作为增量更新添加,而是所有对象(更改或未更改)都可以重新排序,重新编号,间接对象可能变成直接对象,反之亦然,未使用的对象可能会被删除,重复的对象可能会减少到一个,表单字段的字体只读可能会减少到实际使用的字形等 pp

只有这个准备好的 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,进一步审查该方法没有意义。

在我的例子中,我设置签名和签名数据的代码有错误。我不小心交换了值。

所以,而不是:

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。