Как я могу добавить PAdES-LTV, используя iText

Я пытаюсь включить LTV в уже подписанном документе PDF без формата LTV. Я нашел один и тот же пример во всех случаях, как описано в ссылках. Как включить LTV для подписи метки времени, включен iText LTV - как добавить больше CRL?, который, определяет, какова процедура для получения ожидаемого результата. Бывает, что я не работаю, это не дает мне никакой ошибки, но я не добавляю LTV.

Некоторая идея о том, почему во время выполнения следующего кода не дает мне никакой ошибки, но тем не менее я не добавляю LTV.

Это метод, с помощью которого я пытаюсь добавить LTV:

public void addLtv(String src, String dest, OcspClient ocsp, CrlClient crl, TSAClient tsa)
    throws IOException, DocumentException, GeneralSecurityException {
    PdfReader r = new PdfReader(src);
    FileOutputStream fos = new FileOutputStream(dest);
    PdfStamper stp = PdfStamper.createSignature(r, fos, '\0', null, true);
    LtvVerification v = stp.getLtvVerification();
    AcroFields fields = stp.getAcroFields();
    List<String> names = fields.getSignatureNames();
    String sigName = names.get(names.size() - 1);
    PdfPKCS7 pkcs7 = fields.verifySignature(sigName);
    if (pkcs7.isTsp()) {
        v.addVerification(sigName, ocsp, crl,
            LtvVerification.CertificateOption.SIGNING_CERTIFICATE,
            LtvVerification.Level.OCSP_CRL,
            LtvVerification.CertificateInclusion.NO);
    }
    else {
        for (String name : names) {
            v.addVerification(name, ocsp, crl,
                LtvVerification.CertificateOption.WHOLE_CHAIN,
                LtvVerification.Level.OCSP_CRL,
                LtvVerification.CertificateInclusion.NO);
        }
    }
    PdfSignatureAppearance sap = stp.getSignatureAppearance();
    LtvTimestamp.timestamp(sap, tsa, null);
}

версии, с которыми я работаю:

  • itext: 5.5.11
  • Ява: 8

1 ответ

Решение

Как оказалось в этом комментарии

я хочу, чтобы Adobe LTV-Enable

эта задача связана не столько с PAdES (даже несмотря на то, что используются механизмы, введенные в PAdES), но сосредоточена на собственном профиле подписи Adobe, подписи "LTV enabled".

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

LTV включен означает, что вся информация, необходимая для проверки файла (за исключением корневых сертификатов), содержится внутри.

(для деталей и фона прочитайте этот ответ)

Таким образом, реализация способа включения LTV в сигнатуре примера потребовала некоторых проб и ошибок, и я не могу гарантировать, что Adobe примет выходные данные этого кода как "LTV enabled" в будущих версиях Adobe Acrobat.

Кроме того, существующие API-интерфейсы для подписи iText 5 не являются готовыми для этой задачи, потому что (как выяснилось) Adobe требует определенных дополнительных структур, которые код iText не создает. Самый простой способ исправить это - обновить класс iText. LtvVerification в двух аспектах, поэтому я опишу это здесь. В качестве альтернативы можно было использовать отражение Java или скопировать и настроить немного кода; Если вы не можете обновить iText, как показано ниже, вам придется выбрать один из таких альтернативных подходов.

LTV, позволяющий подписи подписанного PDF

В этом разделе показаны дополнения и изменения кода, с помощью которых LTV может включать документы, такие как пример PDF-файла OP sign_without_LTV.pdf,

Подход с использованием iText's LtvVerification учебный класс

Это оригинальный код, который использует LtvVerification класс из API подписи iText. К сожалению, для этого необходимо добавить функциональность в этот класс.

Заделка LtvVerification

IText 5 LtvVerification только класс предлагает addVerification методы, принимающие имя поля подписи. Функциональность этих методов нам нужна также для сигнатур, не связанных с полем формы, например, для сигнатур ответов OCSP. Для этого я добавил следующую перегрузку этого метода:

public boolean addVerification(PdfName signatureHash, Collection<byte[]> ocsps, Collection<byte[]> crls, Collection<byte[]> certs) throws IOException, GeneralSecurityException {
    if (used)
        throw new IllegalStateException(MessageLocalization.getComposedMessage("verification.already.output"));
    ValidationData vd = new ValidationData();
    if (ocsps != null) {
        for (byte[] ocsp : ocsps) {
            vd.ocsps.add(buildOCSPResponse(ocsp));
        }
    }
    if (crls != null) {
        for (byte[] crl : crls) {
            vd.crls.add(crl);
        }
    }
    if (certs != null) {
        for (byte[] cert : certs) {
            vd.certs.add(cert);
        }
    }
    validated.put(signatureHash, vd);
    return true;
}

Кроме того, требуется (согласно спецификации необязательно) запись времени в окончательных словарях VRI. Таким образом, я добавил строку в outputDss метод следующим образом:

...
if (ocsp.size() > 0)
    vri.put(PdfName.OCSP, writer.addToBody(ocsp, false).getIndirectReference());
if (crl.size() > 0)
    vri.put(PdfName.CRL, writer.addToBody(crl, false).getIndirectReference());
if (cert.size() > 0)
    vri.put(PdfName.CERT, writer.addToBody(cert, false).getIndirectReference());
// v--- added line
vri.put(PdfName.TU, new PdfDate());
// ^--- added line
vrim.put(vkey, writer.addToBody(vri, false).getIndirectReference());
...

Некоторые вспомогательные методы низкого уровня

Требуются некоторые вспомогательные методы, работающие с примитивами безопасности. Эти методы в основном были собраны из существующих классов iText (которые нельзя использовать как есть, потому что они являются частными) или получены из кода:

static X509Certificate getOcspSignerCertificate(byte[] basicResponseBytes) throws CertificateException, OCSPException, OperatorCreationException {
    JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
    BasicOCSPResponse borRaw = BasicOCSPResponse.getInstance(basicResponseBytes);
    BasicOCSPResp bor = new BasicOCSPResp(borRaw);

    for (final X509CertificateHolder x509CertificateHolder : bor.getCerts()) {
        X509Certificate x509Certificate = converter.getCertificate(x509CertificateHolder);

        JcaContentVerifierProviderBuilder jcaContentVerifierProviderBuilder = new JcaContentVerifierProviderBuilder();
        jcaContentVerifierProviderBuilder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
        final PublicKey publicKey = x509Certificate.getPublicKey();
        ContentVerifierProvider contentVerifierProvider = jcaContentVerifierProviderBuilder.build(publicKey);

        if (bor.isSignatureValid(contentVerifierProvider))
            return x509Certificate;
    }

    return null;
}

static PdfName getOcspSignatureKey(byte[] basicResponseBytes) throws NoSuchAlgorithmException, IOException {
    BasicOCSPResponse basicResponse = BasicOCSPResponse.getInstance(basicResponseBytes);
    byte[] signatureBytes = basicResponse.getSignature().getBytes();
    DEROctetString octetString = new DEROctetString(signatureBytes);
    byte[] octetBytes = octetString.getEncoded();
    byte[] octetHash = hashBytesSha1(octetBytes);
    PdfName octetName = new PdfName(Utilities.convertToHex(octetHash));
    return octetName;
}

static PdfName getCrlSignatureKey(byte[] crlBytes) throws NoSuchAlgorithmException, IOException, CRLException, CertificateException {
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    X509CRL crl = (X509CRL)cf.generateCRL(new ByteArrayInputStream(crlBytes));
    byte[] signatureBytes = crl.getSignature();
    DEROctetString octetString = new DEROctetString(signatureBytes);
    byte[] octetBytes = octetString.getEncoded();
    byte[] octetHash = hashBytesSha1(octetBytes);
    PdfName octetName = new PdfName(Utilities.convertToHex(octetHash));
    return octetName;
}

static X509Certificate getIssuerCertificate(X509Certificate certificate) throws IOException, StreamParsingException {
    String url = getCACURL(certificate);
    if (url != null && url.length() > 0) {
        HttpURLConnection con = (HttpURLConnection)new URL(url).openConnection();
        if (con.getResponseCode() / 100 != 2) {
            throw new IOException(MessageLocalization.getComposedMessage("invalid.http.response.1", con.getResponseCode()));
        }
        InputStream inp = (InputStream) con.getContent();
        byte[] buf = new byte[1024];
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        while (true) {
            int n = inp.read(buf, 0, buf.length);
            if (n <= 0)
                break;
            bout.write(buf, 0, n);
        }
        inp.close();

        X509CertParser parser = new X509CertParser();
        parser.engineInit(new ByteArrayInputStream(bout.toByteArray()));
        return (X509Certificate) parser.engineRead();
    }
    return null;
}

static String getCACURL(X509Certificate certificate) {
    ASN1Primitive obj;
    try {
        obj = getExtensionValue(certificate, Extension.authorityInfoAccess.getId());
        if (obj == null) {
            return null;
        }
        ASN1Sequence AccessDescriptions = (ASN1Sequence) obj;
        for (int i = 0; i < AccessDescriptions.size(); i++) {
            ASN1Sequence AccessDescription = (ASN1Sequence) AccessDescriptions.getObjectAt(i);
            if ( AccessDescription.size() != 2 ) {
                continue;
            }
            else if (AccessDescription.getObjectAt(0) instanceof ASN1ObjectIdentifier) {
                ASN1ObjectIdentifier id = (ASN1ObjectIdentifier)AccessDescription.getObjectAt(0);
                if ("1.3.6.1.5.5.7.48.2".equals(id.getId())) {
                    ASN1Primitive description = (ASN1Primitive)AccessDescription.getObjectAt(1);
                    String AccessLocation =  getStringFromGeneralName(description);
                    if (AccessLocation == null) {
                        return "" ;
                    }
                    else {
                        return AccessLocation ;
                    }
                }
            }
        }
    } catch (IOException e) {
        return null;
    }
    return null;
}

static ASN1Primitive getExtensionValue(X509Certificate certificate, String oid) throws IOException {
    byte[] bytes = certificate.getExtensionValue(oid);
    if (bytes == null) {
        return null;
    }
    ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(bytes));
    ASN1OctetString octs = (ASN1OctetString) aIn.readObject();
    aIn = new ASN1InputStream(new ByteArrayInputStream(octs.getOctets()));
    return aIn.readObject();
}

static String getStringFromGeneralName(ASN1Primitive names) throws IOException {
    ASN1TaggedObject taggedObject = (ASN1TaggedObject) names ;
    return new String(ASN1OctetString.getInstance(taggedObject, false).getOctets(), "ISO-8859-1");
}

static byte[] hashBytesSha1(byte[] b) throws NoSuchAlgorithmException {
    MessageDigest sh = MessageDigest.getInstance("SHA1");
    return sh.digest(b);
}

(как в MakeLtvEnabled)

Они еще не оптимизированы, конечно, можно сделать их более производительными и элегантными.

Добавление информации о LTV

На основе этих дополнений и помощников можно добавить информацию о LTV, необходимую для подписей с поддержкой LTV, с помощью этого метода. makeLtvEnabled:

public void makeLtvEnabled(PdfStamper stp, OcspClient ocspClient, CrlClient crlClient) throws IOException, GeneralSecurityException, StreamParsingException, OperatorCreationException, OCSPException {
    stp.getWriter().addDeveloperExtension(new PdfDeveloperExtension(PdfName.ADBE, new PdfName("1.7"), 8));
    LtvVerification v = stp.getLtvVerification();
    AcroFields fields = stp.getAcroFields();

    Map<PdfName, X509Certificate> moreToCheck = new HashMap<>();

    ArrayList<String> names = fields.getSignatureNames();
    for (String name : names)
    {
        PdfPKCS7 pdfPKCS7 = fields.verifySignature(name);
        List<X509Certificate> certificatesToCheck = new ArrayList<>();
        certificatesToCheck.add(pdfPKCS7.getSigningCertificate());
        while (!certificatesToCheck.isEmpty()) {
            X509Certificate certificate = certificatesToCheck.remove(0);
            addLtvForChain(certificate, ocspClient, crlClient,
                    (ocsps, crls, certs) -> {
                        try {
                            v.addVerification(name, ocsps, crls, certs);
                        } catch (IOException | GeneralSecurityException e) {
                            e.printStackTrace();
                        }
                    },
                    moreToCheck::put
            );
        }
    }

    while (!moreToCheck.isEmpty()) {
        PdfName key = moreToCheck.keySet().iterator().next();
        X509Certificate certificate = moreToCheck.remove(key);
        addLtvForChain(certificate, ocspClient, crlClient,
                (ocsps, crls, certs) -> {
                    try {
                        v.addVerification(key, ocsps, crls, certs);
                    } catch (IOException | GeneralSecurityException e) {
                        e.printStackTrace();
                    }
                },
                moreToCheck::put
        );
    }
}

void addLtvForChain(X509Certificate certificate, OcspClient ocspClient, CrlClient crlClient, VriAdder vriAdder,
        BiConsumer<PdfName, X509Certificate> moreSignersAndCertificates) throws GeneralSecurityException, IOException, StreamParsingException, OperatorCreationException, OCSPException {
    List<byte[]> ocspResponses = new ArrayList<>();
    List<byte[]> crls = new ArrayList<>();
    List<byte[]> certs = new ArrayList<>();

    while (certificate != null) {
        System.out.println(certificate.getSubjectX500Principal().getName());
        X509Certificate issuer = getIssuerCertificate(certificate);
        certs.add(certificate.getEncoded());
        byte[] ocspResponse = ocspClient.getEncoded(certificate, issuer, null);
        if (ocspResponse != null) {
            System.out.println("  with OCSP response");
            ocspResponses.add(ocspResponse);
            X509Certificate ocspSigner = getOcspSignerCertificate(ocspResponse);
            if (ocspSigner != null) {
                System.out.printf("  signed by %s\n", ocspSigner.getSubjectX500Principal().getName());
            }
            moreSignersAndCertificates.accept(getOcspSignatureKey(ocspResponse), ocspSigner);
        } else {
           Collection<byte[]> crl = crlClient.getEncoded(certificate, null);
           if (crl != null && !crl.isEmpty()) {
               System.out.printf("  with %s CRLs\n", crl.size());
               crls.addAll(crl);
               for (byte[] crlBytes : crl) {
                   moreSignersAndCertificates.accept(getCrlSignatureKey(crlBytes), null);
               }
           }
        }
        certificate = issuer;
    }

    vriAdder.accept(ocspResponses, crls, certs);
}

interface VriAdder {
    void accept(Collection<byte[]> ocsps, Collection<byte[]> crls, Collection<byte[]> certs);
}

( MakeLtvEnabled как makeLtvEnabledV2 )

Пример использования

Для подписанного PDF на INPUT_PDF и поток вывода результатов RESULT_STREAM Вы можете использовать метод выше, как это:

PdfReader pdfReader = new PdfReader(INPUT_PDF);
PdfStamper pdfStamper = new PdfStamper(pdfReader, RESULT_STREAM, (char)0, true);

OcspClient ocsp = new OcspClientBouncyCastle();
CrlClient crl = new CrlClientOnline();
makeLtvEnabledV2(pdfStamper, ocsp, crl);

pdfStamper.close();

(Тестовый метод MakeLtvEnabled testV2 )

Ограничения

Приведенные выше методы работают только с некоторыми упрощающими ограничениями, в частности:

  • метки времени подписи игнорируются,
  • полученные CRL предполагаются прямыми и полными,
  • Предполагается, что полные цепочки сертификатов могут быть построены с использованием записей AIA.

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

Подход с использованием собственного служебного класса

Чтобы избежать необходимости исправления класса iText, этот подход использует необходимый код из приведенных выше методов и LtvVerification класс из API подписи iText и объединяет все в новый служебный класс. Этот класс может LTV включить документ, не требуя исправленной версии iText.

AdobeLtvEnabling учебный класс

Этот класс объединяет код выше и некоторые LtvVerification код в служебный класс для документов, поддерживающих LTV.

К сожалению, копирование здесь приводит к тому, что размер сообщения превышает ограничение в 30000 символов при переполнении стека. Вы можете получить код из GitHub, хотя:

AdobeLtvEnabling.java

Пример использования

Для подписанного PDF на INPUT_PDF и поток вывода результатов RESULT_STREAM Вы можете использовать класс выше, как это:

PdfReader pdfReader = new PdfReader(INPUT_PDF);
PdfStamper pdfStamper = new PdfStamper(pdfReader, RESULT_STREAM, (char)0, true);

AdobeLtvEnabling adobeLtvEnabling = new AdobeLtvEnabling(pdfStamper);
OcspClient ocsp = new OcspClientBouncyCastle();
CrlClient crl = new CrlClientOnline();
adobeLtvEnabling.enable(ocsp, crl);

pdfStamper.close();

(Тестовый метод MakeLtvEnabled testV3 )

Ограничения

Поскольку этот служебный класс просто переупаковывает код из первого подхода, применяются те же ограничения.

За кулисами

Как уже упоминалось в начале, все, что Adobe сообщает нам о профиле подписи "LTV enabled", заключается в

LTV включен означает, что вся информация, необходимая для проверки файла (за исключением корневых сертификатов), содержится в

но они не говорят нам, как именно они ожидают, что информация будет встроена в файл.

Сначала я просто собрал всю эту информацию и удостоверился, что она была добавлена ​​в соответствующие словари PDF для хранилища безопасности документов (Certs, OCSP и CRL).

Но даже при том, что вся информация, необходимая для проверки файла (за исключением корневых сертификатов), содержалась внутри, Adobe Acrobat не рассматривал файл "LTV включен".

Затем я включил документ с помощью Adobe Acrobat и проанализировал различия. Как оказалось, следующие дополнительные данные также были необходимы:

  1. Для подписи каждого ответа OCSP Adobe Acrobat требует наличия соответствующего словаря VRI. В примере PDF OP этот словарь VRI вообще не должен содержать никаких сертификатов, CRL или ответов OCSP, но словарь VRI должен быть там.

    В отличие от этого это не является необходимым для подписей CRL. Это выглядит немного произвольно.

    В соответствии со спецификациями, как ISO 32000-2, так и ETSI EN 319 142-1, использование этих словарей VRI не является обязательным. Для подписей PAdES BASELINE даже есть рекомендация против использования словарей VRI!

  2. Adobe Acrobat ожидает, что каждый словарь VRI будет содержать запись TU, документирующую время создания соответствующего словаря VRI. (Вероятно, TS тоже подойдет, я этого не проверял).

    В соответствии со спецификациями, как ISO 32000-2, так и ETSI EN 319 142-1, использование этих записей TU является необязательным. Для подписей PAdES даже существует рекомендация против использования записей TU или TS!

Таким образом, неудивительно, что по умолчанию информация о LTV, добавляемая приложениями в соответствии со спецификациями PDF, не приводит к сигнатурам с поддержкой LTV, как сообщает Adobe Acrobat.

PS

Очевидно, мне пришлось добавить доверие к некоторому сертификату в Adobe Acrobat, чтобы он вообще мог учесть результат вышеприведенного кода для документа OP "LTV включен". Я выбрал корневой сертификат "CA RAIZ NACIONAL - COSTA RICA v2".

Другие вопросы по тегам