【问题标题】:Need advice on checking signature/certificate of a signed pdf using java需要有关使用 java 检查签名 pdf 的签名/证书的建议
【发布时间】:2019-10-25 02:44:33
【问题描述】:

下面的代码有几个问题。

谷歌搜索,阅读 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,其中包含表单字段和签名字段。受信任的(根)证书位于 keystone 中。

问题一:查看附近的代码:

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

为什么要检查呢?这里可能出了什么问题?

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

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

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

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

问题 3:如果 pdf 在签名后被修改,那么代码仍然会产生消息“PDF 签名验证正确”

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

问题4:见代码:

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

如果证书路径未指向受信任的证书,则会失败。这不是比问题 1 中提到的检查更好的检查吗?

【问题讨论】:

  • 我建议你也看看源代码下载中示例子项目中的 ShowSignature.java。这会进行更多检查,包括证书在签名时是否有效(对于链的其余部分相同),还可以判断签名是否涵盖整个文档。但它也不完美,它不检查链的根是否可信。
  • @TilmanHausherr “这会进行更多检查,包括证书在签名时是否有效” - 此类测试也隐式发生在 OP 的代码中,深入 BouncyCastle 调用中。但是这种隐式测试可能很麻烦。

标签: validation digital-signature pdfbox x509certificate verification


【解决方案1】:

首先,您向我们展示了一些未知来​​源的代码并提出相关问题。由于我们不知道它的上下文,答案可能有点模糊或看起来不符合实际上下文。

问题一:

查看附近的代码:

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

为什么要检查呢?这里可能出了什么问题?

(“附近的代码...”是指确切的代码?由于不清楚,我尝试简单地将注释放入上下文中...)

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

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

所以...

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

问题2:

查看附近的代码:

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

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

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

(通过“靠近...的代码”您的意思是确切的代码?由于不清楚,我假设您的意思正是您引用的代码行)

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

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

问题3:

如果在签名后修改了 pdf,那么代码仍然会产生消息“PDF 签名验证正确”我会认为检查失败!签名后检测pdf被修改的java代码是什么?

这取决于如何 pdf 被修改!有两种选择,一种是通过增量更新应用更改(在这种情况下,原始签名的 PDF 字节被复制而不进行更改,然后附加更改)或其他方式(在这种情况下,原始签名的 PDF 字节不会 构成更改后 PDF 的开始)。

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

但是,在前一种情况下,签名字节没有改变,您的代码将显示“PDF 签名验证正确”。要捕捉这种变化,您还必须检查签名的 PDF 字节是否是整个 PDF,除了为 CMS 签名容器保留的位置,或者是否还有其他字节未考虑在内。

有关某些详细信息,请阅读this answer,对于被认为允许的更改,请阅读this answer

问题4:

见代码:

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

如果证书路径未指向受信任的证书,则会失败。这不是比问题 1 中提到的检查好得多吗?

如上所述,导致问题1的代码根本不是检查,它是关于确定最终应接受检查的证书。不过,这里的代码实际上采用了先前确定的证书并实际检查它。

精髓

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

  • 确定一个签名者证书候选(您的代码是根据SignerIdentifier 值执行此操作的;但是,由于它本身没有签名,现在人们认为仅此标准是不够的,并且还使用签名属性( ESSCertIDESSCertIDv2);
  • 验证候选证书是否可用于验证加密签名值(在您的情况下,在signer.verify(signerInformationVerifier) 期间);
  • 验证已签名文档范围的哈希值是否与 messageDigest 已签名属性的值匹配(在您的情况下,也可以在 signer.verify(signerInformationVerifier) 期间);
  • 验证签名者证书是否可信(在您的情况下,在certPathValidator.validate 期间)。

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

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

【讨论】:

  • 非常感谢 - 您的回答对我非常有帮助并解决了我的问题。
猜你喜欢
  • 2012-08-12
  • 1970-01-01
  • 1970-01-01
  • 2017-06-05
  • 2012-01-28
  • 1970-01-01
  • 2016-05-01
  • 2011-01-21
  • 2017-11-28
相关资源
最近更新 更多