【问题标题】:Verifying PDF Signature in Java using Bouncy Castle and PDFBox使用 Bouncy Castle 和 PDFBox 在 Java 中验证 PDF 签名
【发布时间】:2017-12-04 05:09:52
【问题描述】:

我正在尝试用 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) 函数,所以这不是错误。

当我尝试使用充气城堡验证签名时,我收到一条异常消息:message-digest 属性值与计算值不匹配

我猜这是因为签名的原始 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。这不是真正的 PDF。请参阅源代码下载中的 ShowSignature.java 示例,了解如何验证签名。如果这没有帮助,请编辑您的问题。
  • @user3362334 你能记录下 PDFUtils() 的来源吗?它不在 apache pdfbox-2.0.18 中......
  • @user2677034,你能分享一下 PDFUtils 和 SignatureInfo 的 maven repo 吗?
  • @User、PDFUtils 和 SignatureInfo 均来自 BouncyCastle
  • Thnx alot @user2677034 ,我已经为 bouncyCastle 添加了 maven,但给出了编译错误。有什么线索吗?

标签: java pdf digital-signature bouncycastle pdfbox


【解决方案1】:

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

我正在使用 Apache PDFBox 2.0.6 来获取签名和已签名的原始 PDF

如果“签名的原始 PDF” 是指在进入签名过程之前的 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,进一步审查该方法是没有意义的。

【讨论】:

    【解决方案2】:

    在我的情况下,我设置签名和签名数据的代码中存在错误。我不小心交换了值。

    所以,而不是:

    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。

    【讨论】:

    • 我对 javadoc 做了一些改进,希望对下一个人有所帮助。 svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/…
    • "that when a signature is created signature fields are added and hash is calculated" 最近添加了您还可以在PDF文档中签名现有的签名字段。因此,并不总是添加签名字段(有时它们确实存在)。
    猜你喜欢
    • 1970-01-01
    • 2016-10-01
    • 1970-01-01
    • 2015-07-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-17
    • 1970-01-01
    相关资源
    最近更新 更多