Нужен совет по проверке подписи / сертификата подписанного PDF с использованием Java - PullRequest
1 голос
/ 10 июня 2019

Несколько вопросов к коду ниже.

googled, прочитайте 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-файле, который содержит поля формы и поле подписи.доверенные (корневые) сертификаты лежат в основе.

Вопрос 1: См. код рядом:

// 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 верна»

Iдумал бы проверка не удалась!Что такое Java-код, чтобы обнаружить, что PDF-файл был изменен после подписания?

Вопрос 4: См. Код:

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

Сбой, если путь к сертификату не ведет к доверенному сертификату.Разве это не намного лучшая проверка, чем проверка, указанная в вопросе 1?

1 Ответ

2 голосов
/ 14 июня 2019

Во-первых, вы показываете нам код из неизвестного источника и задаете вопросы о нем.Поскольку мы не знаем его контекста, ответы могут быть немного расплывчатыми или не соответствовать реальному контексту.

Вопрос 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-файла получаются сертификаты, принадлежащие подписавшему».- Ну, строго говоря, он извлекает кандидатов для сертификата подписавшего из сертификатов, хранящихся в контейнере подписи, хранящемся в PDF-файле, соответствующего SignerIdentifier.

"Разве это не дублируется вcode ... "- Нет, код там создает BouncyCastle SignerInformationVerifier, который эффективно объединяет несколько служебных объектов верификатора для различных аспектов подписи.Этот объект инициализируется сертификатом кандидата на подпись, полученным в предыдущем коде.Таким образом, без дублирования.

Вопрос 3:

Если PDF-файл был изменен после подписи, то код по-прежнему выдает сообщение «Проверка подписи PDF верна». Я бы подумал, что проверкатерпит неудачу!Что такое Java-код, чтобы обнаружить, что PDF был изменен после подписания?

Это зависит от как PDF был изменен !Есть два варианта: либо изменения были применены посредством инкрементного обновления (в этом случае оригинальные подписанные байты PDF копируются без изменений, а изменения добавляются после этого), либо иным образом (в этом случае оригинальные подписанные байты PDF не не представляет собой начало измененного PDF).

В последнем случае изменяются первоначально подписанные байты, и ваш код напечатает «Проверка подписи PDF не удалась».

В первом случаеоднако подписанные байты не изменяются, и в вашем коде будет отображаться «Проверка подписи PDF верна».Чтобы отследить изменения такого типа, вам также необходимо проверить, являются ли байты подписанного PDF целым PDF, за исключением места, зарезервированного для контейнера подписи CMS, или других байтов, не учитываемых.

Длянекоторые подробности читайте этот ответ , а для изменений, которые считаются разрешенными, читайте этот ответ .

Вопрос 4:

См. код:

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

Сбой, если путь к сертификату не ведет к доверенному сертификату. Разве это не намного лучшая проверка, чем проверка, указанная в вопросе 1?

Как уже говорилось выше, код, приводящий к вопросу 1, вовсе не является проверкой , а приблизительно определяет сертификат, который в конечном итоге подлежит проверке. Код здесь, на самом деле, принимает этот ранее определенный сертификат и фактически проверяет его.

Квинтэссенция

Вопросы 1, 2 и 4 по существу касаются понимания шагов, которые необходимо предпринять при проверке контейнера подписи CMS. В частности, вы должны

  • определяет кандидата на сертификат сертификата подписчика (ваш код делает это на основе значения SignerIdentifier; так как он сам по себе не подписан, однако в настоящее время один этот критерий считается недостаточным и дополнительно использует подписанные атрибуты (ESSCertID или ESSCertIDv2);
  • проверить, можно ли использовать сертификат-кандидат для проверки значения криптографической подписи (в вашем случае во время signer.verify(signerInformationVerifier));
  • убедитесь, что хэш диапазонов подписанных документов соответствует значению атрибута со знаком messageDigest (в вашем случае также во время signer.verify(signerInformationVerifier));
  • проверьте, можно ли доверять сертификату подписавшего (в вашем случае во время certPathValidator.validate).

Вопрос 3, по сути, касается понимания дополнительных шагов, которые необходимо предпринять при проверке контейнера подписи CMS, встроенного в PDF. В частности, вы должны

  • проверьте, охватывают ли подписанные байтовые диапазоны весь PDF, кроме заполнителя, оставленного для контейнера подписи (не выполняется вашим кодом).
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...