有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

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

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

我使用ApachePDFBOx2.0.6获取签名和签名的原始PDF,然后使用BouncyCastle验证分离的签名(计算原始文件的哈希,使用签名者的公钥验证签名并比较结果)

我读取了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没有。此外,save origPDF的大小为21KB,而原始PDF的大小为15KB。这可能是因为签名字段

但是,当我尝试从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)函数,所以这不是一个错误

当我尝试使用bouncy castle验证签名时,我得到一个异常消息:消息摘要属性值与计算值不匹配

我想这是因为签名的原始PDF和我使用doc.getSignatureDictionaries().get(0).getSignedContent(signedPDF);从PDFBox获得的PDF不一样

这是我的bouncy castle验证码:

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;

}

共 (2) 个答案

  1. # 1 楼答案

    您似乎对getSignedContent方法和PDF签名有误解

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

    如果您所说的“签名的原始PDF”是指进入签名过程之前的PDF,那么您的任务的第二部分对于一般签名PDF是不可能的

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

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

    然而,另一方面,这可能意味着也会发生以下一些变化:

    • 可以从头创建新的签名字段
    • 可在文件中增加一页,用于签名可视化
    • 额外的签名可视化(非活动图像或实际签名表单字段小部件)可以添加到每个页面
    • 可能会创建表单字段缺少的外观
    • 签名应用程序可以将其名称添加到元数据条目中作为文档处理器,最后更改的日期和时间可以更新为签名时间
    • 对于预先存在的空签名字段,由该字段的字段锁定字典指示的表单字段可以设置为只读
    • 等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对象的起始偏移量(从文档开始)的交叉引用;由于缺少间隙,其前一个位置之后的字节已经移动,现在移动到该位置的偏移量是错误的

    因此,您的^ {< CD2>}仅包含符号字节的集合,它与您认为原始文件的文件非常不同。p>


    您的verifySig完全忽略签名字段值字典的子过滤器。根据该值,使用getContents检索的签名字节可能具有完全不同的内容

    因此,如果没有您的签名PDF,进一步审查该方法是没有意义的

  2. # 2 楼答案

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

    因此,不是:

    signedData = pdfUtils.getSignature(signedData);
    byte[] sig = pdfUtils.getSignedContent(signedData);
    

    应该是:

    byte[] sig = pdfUtils.getSignature(signedData);
    signedData = pdfUtils.getSignedContent(signedData); 
    

    现在,它起作用了。我测试它时使用的文件是使用adbe.pkcs7.detached签名的。然而,如果使用其他的签名方法,它将不起作用

    所以,感谢@Tilman Hausherr为我指出了展示签名。java示例。 签名验证就是这样做的

    另外,还要感谢@mkl的详细解释

    我现在了解到,当创建签名时,会添加签名字段,并根据新值计算哈希值。这就是为什么核查是有效的。您不需要没有签名字段的原始PDF