Need advice on checking signature/certificate of a

2019-08-08 06:00发布

Several questions to the code below.

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 {
        }
    }
}

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

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

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

Question 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);                                                                       

Question 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?

Question 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条回答
兄弟一词,经得起流年.
2楼-- · 2019-08-08 07:06

First off, you show us code from some unknown source and ask questions about it. As we don't know its context, answers may be a bit vague or appear not to fit the actual context.

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

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

(By "code near ..." you mean which code exactly? As that's unclear, I try to simply put the comment into context...)

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).

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).

So...

  • "Why would one check that?" - Nothing is checked yet.
  • "What could go wrong here?" - Probably the claimed signer certificate cannot be found among the certificates in the signature container, or multiple candidates are found. Your code does not offer a strategy for the former case, not even a warning or error is printed. In the latter case it tests each candidate. Usually verification will succeed with at most one of the candidate certificates.

Question 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);

(By "code near ..." you mean which code exactly? As that's unclear, I assume you mean exactly the code lines you quoted)

"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.

"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.

Question 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?

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).

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

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.

Question 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?

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.

Quintessence

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

  • 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).

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

  • check whether the signed byte ranges encompass all the PDF except the placeholder left for the signature container (not done by your code).
查看更多
登录 后发表回答