需要有关使用 java 检查 signature/certificate 签名 pdf 的建议

Need advice on checking signature/certificate of a signed pdf using java

下面代码的几个问题。

用谷歌搜索,阅读 java文档

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,其中包含表单字段和签名字段。受信任的(根)证书在 keystone 中。

问题一:见附近代码:

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

为什么要检查那个?这里会出什么问题?

问题二:见附近代码:

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

这会从属于签名者的 pdf 中获取证书。这不是在附近的代码中重复了吗:

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

问题 3:如果 pdf 在签名后被修改,那么代码仍然会产生消息 "PDF signature verification is correct"

我还以为检查失败了呢! java 检测签名后 pdf 被修改的代码是什么?

问题四:见代码:

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

如果证书路径不指向受信任的证书,则此操作失败。这不是比问题 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 对象已用于将签名容器中包含的证书之一标识为 claimed signer certificate(是的,实际上有一个循环遍历多个可能的匹配项,但常见的情况是只找到一个匹配项,其他一切都应该被认为是可疑的)。

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

所以...

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

问题 2:

See code near:

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

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 中存储的签名容器中的证书中检索签名者证书的 candidates

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

问题 3:

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的开始)。

在后一种情况下,最初签名的字节被更改,您的代码将打印“PDF 签名验证失败”。

不过,在前一种情况下,签名字节没有改变,您的代码将显示“PDF 签名验证正确”。要捕获这种变化,还需要检查签名后的PDF字节是否是除CMS签名容器预留位置以外的整个PDF,或者是否有其他字节未占。

有关详细信息,请阅读 this answer and for changes considered allowed read this answer

问题 4:

See code:

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的代码根本不是检查,它是关于确定证书的最终要接受检查。不过,此处的代码实际上采用了 先前确定的 证书,并且实际上 检查 它。

精华

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

  • 确定一个 签名者证书候选人 (您的代码基于 SignerIdentifier 值执行此操作;因为它本身没有签名,但是,现在人们单独考虑这个标准不足并另外使用签名属性(ESSCertIDESSCertIDv2);
  • 验证候选证书可用于验证加密签名值(在您的情况下 signer.verify(signerInformationVerifier));
  • 验证已签名文档范围的哈希值是否与 messageDigest 已签名属性的值匹配(在您的情况下也在 signer.verify(signerInformationVerifier) 期间);
  • 验证签署者证书是否可信(在您的情况下 certPathValidator.validate)。

问题 3 本质上是关于了解在验证 PDF 中集成的 CMS 签名容器时要采取的额外步骤。特别是你必须

  • 检查签名的字节范围是否包含除了签名容器的占位符之外的所有 PDF(不是由您的代码完成)。