需要有关使用Java检查签名pdf的签名/证书的建议 [英] Need advice on checking signature/certificate of a signed pdf using java

查看:240
本文介绍了需要有关使用Java检查签名pdf的签名/证书的建议的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

以下代码有几个问题。

Several questions to the code below.

googled,阅读javadoc

googled, read javadoc

import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStoreBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.*;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.jcajce.util.MessageDigestUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Store;
import org.bouncycastle.util.encoders.Hex;

import javax.security.cert.CertificateEncodingException;
import javax.xml.bind.DatatypeConverter;
import java.io.*;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.*;
import java.text.SimpleDateFormat;
import java.util.*;

import static java.security.AlgorithmParameterGenerator.getInstance;

public class PDFProcess {
    public static void main(String[] args) {
        System.out.println("Assume customer has signed the prefilled.pdf.  Read prefilled.pdf");
        PDDocument document = null;

        /*
         * processes file anacreditForm-signed trusted which has password protection.  both owner password 1234 or user password abce will work
         *
         */
        try {
            File signedFile = new File("anacreditForm-signed expired not locked.pdf");
            document = PDDocument.load(signedFile, "1234");

            System.out.println("Number of pages" + document.getNumberOfPages());

            PDDocumentCatalog pdCatalog = document.getDocumentCatalog();
            PDAcroForm pdAcroForm = pdCatalog.getAcroForm();

            for (PDField pdField : pdAcroForm.getFields()) {
                System.out.println("Values found: " + pdField.getValueAsString());
            }

            System.out.println("Signed? " + pdAcroForm.isSignaturesExist());
            if (pdAcroForm.isSignaturesExist()) {
                PDSignatureField signatureField = (PDSignatureField) pdAcroForm.getField("signatureField");
                System.out.println("Name:         " + signatureField.getSignature().getName());
                System.out.println("Contact Info: " + signatureField.getSignature().getContactInfo());

                Security.addProvider(new BouncyCastleProvider());
                List<PDSignature> signatureDictionaries = document.getSignatureDictionaries();
                X509Certificate cert;
                Collection<X509Certificate> result = new HashSet<X509Certificate>();
                // Then we validate signatures one at the time.
                for (PDSignature signatureDictionary : signatureDictionaries) {
                    // NOTE that this code currently supports only "adbe.pkcs7.detached", the most common signature /SubFilter anyway.
                    byte[] signatureContent = signatureDictionary.getContents(new FileInputStream(signedFile));
                    byte[] signedContent = signatureDictionary.getSignedContent(new FileInputStream(signedFile));
                    // Now we construct a PKCS #7 or CMS.
                    CMSProcessable cmsProcessableInputStream = new CMSProcessableByteArray(signedContent);
                    try {
                        CMSSignedData cmsSignedData = new CMSSignedData(cmsProcessableInputStream, signatureContent);
                        // get certificates
                        Store<?> certStore = cmsSignedData.getCertificates();
                        // get signers
                        SignerInformationStore signers = cmsSignedData.getSignerInfos();
                        // variable "it" iterates all signers
                        Iterator<?> it = signers.getSigners().iterator();
                        while (it.hasNext()) {
                            SignerInformation signer = (SignerInformation) it.next();
                            // get all certificates for a signer
                            Collection<?> certCollection = certStore.getMatches(signer.getSID());
                            // variable "certIt" iterates all certificates of a signer
                            Iterator<?> certIt = certCollection.iterator();
                            while (certIt.hasNext()) {
                                // print details of each certificate
                                X509CertificateHolder certificateHolder = (X509CertificateHolder) certIt.next();
                                System.out.println("Subject:      " + certificateHolder.getSubject());
                                System.out.println("Issuer:       " + certificateHolder.getIssuer());
                                System.out.println("Valid from:   " + certificateHolder.getNotBefore());
                                System.out.println("Valid to:     " + certificateHolder.getNotAfter());
                                //System.out.println("Public key:   " + Hex.toHexString(certificateHolder.getSubjectPublicKeyInfo().getPublicKeyData().getOctets()));

                                CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
                                InputStream in = new ByteArrayInputStream(certificateHolder.getEncoded());
                                X509Certificate cert2 = (X509Certificate) certFactory.generateCertificate(in);
                                // the validity of the certificate isn't verified, just the fact that one of the certs matches the given signer
                                SignerInformationVerifier signerInformationVerifier = new JcaSimpleSignerInfoVerifierBuilder()
                                            .build(cert2);
                                if (signer.verify(signerInformationVerifier)){
                                    System.out.println("PDF signature verification is correct");
                                } else { System.out.println ("PDF signature verification failed");}

                                StringBuilder encodedChain = new StringBuilder();
                                encodedChain.append("-----BEGIN CERTIFICATE-----\n");
                                encodedChain.append(new String(Base64.getEncoder().encode(cert2.getEncoded())));
                                encodedChain.append("\n-----END CERTIFICATE-----\n");
                                System.out.println(encodedChain.toString());

                                //System.out.println("Public key:   " + DatatypeConverter.printHexBinary(certificateHolder.getSubjectPublicKeyInfo().getPublicKeyData().getBytes()));
                                // SerialNumber isi BigInteger in java and hex value in Windows/Mac/Adobe
                                System.out.println("SerialNumber: " + certificateHolder.getSerialNumber().toString(16));

                                //result.add(new JcaX509CertificateConverter().getCertificate(certificateHolder));

                                CertificateFactory certificateFactory2 = CertificateFactory.getInstance("X.509", new BouncyCastleProvider());
                                InputStream is = new ByteArrayInputStream(certificateHolder.getEncoded());

                                KeyStore keyStore = PKISetup.createKeyStore();

                                PKIXParameters parameters = new PKIXParameters(keyStore);
                                parameters.setRevocationEnabled(false);

                                ArrayList<X509Certificate> start = new ArrayList<>();
                                start.add(cert2);
                                CertificateFactory certFactory3 = CertificateFactory.getInstance("X.509");
                                CertPath certPath = certFactory3.generateCertPath(start);
                                //CertPath certPath = certificateFactory.generateCertPath(is, "PKCS7"); // Throws Certificate Exception when a cert path cannot be generated
                                CertPathValidator certPathValidator = CertPathValidator.getInstance("PKIX", new BouncyCastleProvider());

                                // verifies if certificate is signed by trust anchor available in keystore.  For example jsCAexpired.cer was removed as trust anchor - all certificates signed by jsCAexpired.cer will fail the check below
                                PKIXCertPathValidatorResult validatorResult = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, parameters); // This will throw a CertPathValidatorException if validation fails
                                System.out.println("Val result:  " + validatorResult );
                                System.out.println("Subject was: " + cert2.getSubjectDN().getName());
                                System.out.println("Issuer was:  " + cert2.getIssuerDN().getName());
                                System.out.println("Trust Anchor CA Name:  " + validatorResult.getTrustAnchor().getCAName());
                                System.out.println("Trust Anchor CA:       " + validatorResult.getTrustAnchor().getCA());
                                System.out.println("Trust Anchor Issuer DN:" + validatorResult.getTrustAnchor().getTrustedCert().getIssuerDN());
                                System.out.println("Trust Anchor SubjectDN:" + validatorResult.getTrustAnchor().getTrustedCert().getSubjectDN());
                                System.out.println("Trust Cert Issuer UID:  " + validatorResult.getTrustAnchor().getTrustedCert().getIssuerUniqueID());
                                System.out.println("Trust Cert Subject UID: " + validatorResult.getTrustAnchor().getTrustedCert().getSubjectUniqueID());

                                System.out.println("Trust Cert SerialNumber: " + validatorResult.getTrustAnchor().getTrustedCert().getSerialNumber().toString(16));
                                System.out.println("Trust Cert Valid From:   " + validatorResult.getTrustAnchor().getTrustedCert().getNotBefore());
                                System.out.println("Trust Cert Valid After:  " + validatorResult.getTrustAnchor().getTrustedCert().getNotAfter());
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }   //this.testValidateSignatureValidationTest();

            document.close();
        } catch (InvalidPasswordException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }
    }
}

代码读取受密码保护的pdf,其中包含表单字段和签名字段。

The code reads in a password protected pdf which contains form fields and a signature field. the trusted (root) certificates are in a keystone.

问题1:请参见附近的代码:

Question 1: See code near:

// the validity of the certificate isn't verified, just the fact that one of the certs matches the given signer

为什么要检查一个?

问题2:看到附近的代码:

Question 2: See code near:

Collection<?> certCollection = certStore.getMatches(signer.getSID());   

这将从pdf文件中获取属于签名者的证书。

This gets certificates out of the pdf that belong to the signer. Isn't that duplicated in the code near:

SignerInformationVerifier signerInformationVerifier = new JcaSimpleSignerInfoVerifierBuilder().build(cert2);                                                                       

问题3:如果在签名后对pdf进行了修改,则代码仍会生成消息 PDF签名验证是正确的。

Question 3: if the pdf was modified after signature then the code still produces the message "PDF signature verification is correct"

我本以为检查失败!

问题4:请参见代码:

PKIXCertPathValidatorResult validatorResult = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, parameters); 

如果证书路径未生成受信任的证书,则此操作将失败。

This fails if the certificate path does not lead to a trusted certificate. Isn't this a much better check than the check referenced to in question 1?

推荐答案

首先,您显示的是问题1中提到的支票吗?我们从一些未知来​​源获取代码,并提出有关问题。由于我们不知道它的上下文,答案可能有点含糊或似乎不适合实际上下文。


请参阅附近的代码

See code near:

// the validity of the certificate isn't verified, just the fact that one of the certs matches the given signer

这一事实相符,为什么一个要检查呢?这里可能出什么问题?

Why would one check that? What could go wrong here?

(通过附近的代码...,您指的是确切的代码?因为不清楚,我尝试简单地将注释放入上下文中...)

这时发生的一切就是当前 SignerInfo 对象中的 SignerIdentifier 对象已用于将签名容器中包含的证书之一标识为声明的签名者证书(是的,实际上在多个可能的匹配项上存在一个循环,但通常的情况是找到一个完全匹配项,其他所有内容都应视为可疑)。

At this point all that has happened is that for the current SignerInfo object the SignerIdentifier object therein has been used to identify one of the certificates contained in the signature container as claimed signer certificate (yes, actually there is a loop over multiple possible matches but the common case is to find exactly one match, everything else should be considered suspicious).

因此,代码尚未真正 verified 证书,但它已确定稍后再验证哪个证书(并用来验证签名)。

Thus, the code has not really verified a certificate yet but it has determined which certificate to verify later (and to verify the signature with).

所以...


  • 为什么要检查? -还没有检查。

  • 这里可能出什么问题了? -可能在签名容器的证书中找不到声明的签名者证书,或者找到了多个候选者。
    您的代码没有为前一种情况提供策略,甚至没有打印警告或错误。在后一种情况下,它将测试每个候选人。通常,验证最多将使用其中一个候选证书。


查看附近的代码:

See code near:

Collection certCollection = certStore.getMatches(signer.getSID());

这将从pdf文件中获取属于签名者的证书。这不是重复的代码吗?

This gets certificates out of the pdf that belong to the signer. Isn't that duplicated in the code near:

SignerInformationVerifier signerInformationVerifier = new JcaSimpleSignerInfoVerifierBuilder().build(cert2);


(由靠近...的代码...表示确切地说,这是不清楚的,我认为您的意思恰恰是所引用的代码行)。

这会从属于签名者的pdf中获取证书。 -好吧,严格来说,它从与 SignerIdentifier 匹配的PDF中存储的签名容器中存储的证书中检索签名者证书的候选人

"This gets certificates out of the pdf that belong to the signer." - Well, strictly speaking it retrieves candidates for the signer certificate from the certificates stored in the signature container stored in the PDF matching the SignerIdentifier.

不是在代码中重复的…… -不,那里的代码构造了一个BouncyCastle SignerInformationVerifier ,它有效地捆绑了许多用于签名不同方面的验证程序实用程序对象。使用在先前代码中检索的候选签名者证书初始化此对象。因此,没有重复。

"Isn't that duplicated in the code ..." - No, the code there constructs a BouncyCastle SignerInformationVerifier which effectively bundles a number of verifier utility objects for different aspects of the signature. This object is initialized with the candidate signer certificate retrieved in the former code. Thus, no duplication.


如果在签名后修改了pdf,则代码仍然会产生消息 PDF签名验证正确。我本以为检查失败!

if the pdf was modified after signature then the code still produces the message "PDF signature verification is correct" I would have thought the check fails! What is the java code to detect that the pdf was modified after signing?

这取决于如何 pdf被修改!有两种选择,一种是通过增量更新来应用更改(在这种情况下,原始签名的PDF字节被复制而没有更改,并且此后附加了更改),或者在其他情况下(在原始签名的PDF字节不被添加的情况下) 构成更改后的PDF的开头。)

It depends on how the pdf was modified! There are two options, either changes were applied by means of an incremental update (in which case the original signed PDF bytes are copied without change and changes are appended thereafter) or otherwise (in which case the original signed PDF bytes do not constitute the start of the changed PDF).

在后一种情况下,原始签名的字节已更改,您的代码将打印 PDF签名验证失败。

In the latter case the originally signed bytes are changed and your code will print "PDF signature verification failed".

但是,在前一种情况下,已签名的字节不变,并且您的代码将显示 PDF签名验证正确。为了进行这种更改,您还必须检查已签名的PDF字节是否是整个PDF(除了为CMS签名容器保留的位置),还是没有其他字节占空。

In the former case, though, the signed bytes are unchanged and your code will show "PDF signature verification is correct". To catch this kind of change, you will also have to check whether the signed PDF bytes are the whole PDF except for the place reserved for the CMS signature container, or whether there are other bytes not accounted for.

有关某些详细信息,请阅读此答案;有关被认为允许的更改,请阅读此答案

For some details read this answer and for changes considered allowed read this answer.


查看代码:

See code:

PKIXCertPathValidatorResult validatorResult = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, parameters);

如果证书路径未生成可信证书,则此操作将失败。这不是比问题1中引用的支票好得多的支票吗?

This fails if the certificate path does not lead to a trusted certificate. Isn't this a much better check than the check referenced to in question 1?

如上所述,导致问题1的代码不是完全是 check ,这是关于确定最终要进行检查的证书。不过,这里的代码实际上是使用先前确定的证书并实际上是检查

As said above, the code leading to the question 1 is not a check at all, it is about determining the certificate which eventually shall be subjected to checks. The code here, though, actually takes that previously determined certificate and actually checks it.

问题1、2和4本质上是关于了解验证CMS签名容器时应采取的步骤。特别是,您必须

Questions 1, 2, and 4 essentially are about understanding the steps to take when verifying a CMS signature container. In particular you have to


  • 确定一个签名者证书候选者(您的代码是根据 SignerIdentifier做到的值;但是,由于此值本身不是已签名的,因此,如今人们认为仅凭此条件是不够的,还使用了已签名的属性( ESSCertID ESSCertIDv2 );

  • 验证证书候选者是否可用于验证密码签名值(在您的情况下, signer.verify(signerInformationVerifier));

  • 验证签名文档范围的哈希值是否与 messageDigest 签名属性(在您的情况下,也在 signer.verify(signerInformationVerifier)中);

  • verify可以信任签名者证书(在您的情况下,是 certPathValidator.validate )。

  • determine a signer certificate candidate (your code does it based on the SignerIdentifier value; as this is not itself signed, though, one nowadays considers this criterion alone insufficient and additionally uses signed attributes (ESSCertID or ESSCertIDv2);
  • verify that the certificate candidate can be used to validate the cryptographic signature value (in your case during signer.verify(signerInformationVerifier));
  • verify that the hash of the signed document ranges matches the value of the messageDigest signed attribute (in your case also during signer.verify(signerInformationVerifier));
  • verify that the signer certificate can be trusted (in your case during certPathValidator.validate).

问题3本质上是关于了解何时采取的其他步骤验证集成在PDF中的CMS签名容器。特别是,您必须

Question 3 essentially is about understanding the additional steps to takes when verifying a CMS signature container integrated in a PDF. In particular you have to


  • 检查已签名的字节范围是否包含所有PDF,除了留给签名容器的占位符(未通过代码完成)。 / li>
  • check whether the signed byte ranges encompass all the PDF except the placeholder left for the signature container (not done by your code).

这篇关于需要有关使用Java检查签名pdf的签名/证书的建议的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆