Как поставить цифровую подпись для динамически создаваемого PDF-документа с помощью PDFBox?

Простите! Я плохо разбираюсь в java.
Пожалуйста, исправляйте меня там, где я ошибаюсь, и улучшайте там, где я беден!

Я пытаюсь поставить цифровую подпись для динамически созданного PDF-файла с помощью PDFBox с помощью следующей программы:

Задачи в программе:
(i) Создание шаблона PDF
(ii) Обновление ByteRange, xref, startxref
(iii) Создание исходного документа для создания подписи
(iv ) Создание отдельной цифровой подписи в конверте
(v) Создание PDF-документа с цифровой подписью путем объединения исходной части документа - I, отдельной подписи и исходной части PDF - II

Наблюдения:
(i) pdfFileOutputStream.write (documentOutputStream.toByteArray ()); создает шаблон PDF-документа с видимой подписью.

(ii) Создается какой-то подписанный PDF-документ, но есть ошибки (а) недействительные токены и (б) несколько ошибок парсера
(теперь исправлены под умелым руководством MKL!)

Предложите мне следующее:

(i) Как добавить текст подписи в видимую подпись на слое 2.

Заранее спасибо!

    package digitalsignature;

    import java.awt.geom.AffineTransform;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.security.Signature;
    import java.util.ArrayList;
    import org.bouncycastle.cert.X509CertificateHolder;
    import org.bouncycastle.cert.jcajce.JcaCertStore;
    import org.bouncycastle.cms.CMSProcessableByteArray;
    import org.bouncycastle.cms.CMSTypedData;
    import org.bouncycastle.cms.SignerInfoGenerator;
    import org.bouncycastle.cms.SignerInfoGeneratorBuilder;
    import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
    import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
    import org.bouncycastle.util.Store;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.security.KeyStore;
    import java.security.PrivateKey;
    import java.security.cert.CertStore;
    import java.security.cert.Certificate;
    import java.security.cert.CollectionCertStoreParameters;
    import java.security.cert.X509Certificate;
    import java.text.DecimalFormat;
    import java.text.SimpleDateFormat;
    import java.util.Arrays;
    import java.util.Calendar;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;

    import java.util.Map;
    import org.apache.pdfbox.cos.COSArray;
    import org.apache.pdfbox.cos.COSDictionary;
    import org.apache.pdfbox.cos.COSName;
    import org.apache.pdfbox.pdmodel.PDDocument;
    import org.apache.pdfbox.pdmodel.PDPage;
    import org.apache.pdfbox.pdmodel.PDResources;
    import org.apache.pdfbox.pdmodel.common.PDRectangle;
    import org.apache.pdfbox.pdmodel.common.PDStream;
    import org.apache.pdfbox.pdmodel.edit.PDPageContentStream;
    import org.apache.pdfbox.pdmodel.font.PDFont;
    import org.apache.pdfbox.pdmodel.font.PDType1Font;
    import org.apache.pdfbox.pdmodel.graphics.xobject.PDJpeg;
    import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectForm;
    import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
    import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
    import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
    import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
    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.cms.CMSSignedData;
    import org.bouncycastle.cms.CMSSignedDataGenerator;
    import org.bouncycastle.cms.CMSSignedGenerator;
    import org.bouncycastle.jce.provider.BouncyCastleProvider;


    public class AffixSignature {
        String path = "D:\\reports\\";
        String onlyFileName = "";
        String pdfExtension = ".pdf";
        String pdfFileName = "";
        String pdfFilePath = "";
        String signedPdfFileName = "";
        String signedPdfFilePath = "";
        String ownerPassword = "";
        String tempSignedPdfFileName = "";
        String tempSignedPdfFilePath = "";
        String userPassword = "";
        String storePath = "resources/my.p12";
        String entryAlias = "signerCert";
        String keyStorePassword = "password";
        ByteArrayOutputStream documentOutputStream = null;
        private Certificate[] certChain;
        private static BouncyCastleProvider BC = new BouncyCastleProvider();
        int offsetContentStart = 0;
        int offsetContentEnd = 0;
        int secondPartLength = 0;
        int offsetStartxrefs = 0;
        String contentString = "";
        OutputStream signedPdfFileOutputStream;
        OutputStream pdfFileOutputStream;

        public AffixSignature() {
        try {
            SimpleDateFormat timeFormat = new SimpleDateFormat("hh_mm_ss");

            onlyFileName = "Report_" + timeFormat.format(new Date());
            pdfFileName = onlyFileName + ".pdf";
            pdfFilePath = path + pdfFileName;
            File pdfFile = new File(pdfFilePath);
            pdfFileOutputStream = new FileOutputStream(pdfFile);

            signedPdfFileName = "Signed_" + onlyFileName + ".pdf";
            signedPdfFilePath = path + signedPdfFileName;
            File signedPdfFile = new File(signedPdfFilePath);
            signedPdfFileOutputStream = new FileOutputStream(signedPdfFile);

            String tempFileName = "Temp_Report_" + timeFormat.format(new Date());
            String tempPdfFileName = tempFileName + ".pdf";
            String tempPdfFilePath = path + tempPdfFileName;
            File tempPdfFile = new File(tempPdfFilePath);
            OutputStream tempSignedPdfFileOutputStream = new FileOutputStream(tempPdfFile);

            PDDocument document = new PDDocument();
            PDDocumentCatalog catalog = document.getDocumentCatalog();
            PDPage page = new PDPage(PDPage.PAGE_SIZE_A4);
            PDPageContentStream contentStream = new PDPageContentStream(document, page);


            PDFont font = PDType1Font.HELVETICA;
            Map<String, PDFont> fonts = new HashMap<String, PDFont>();
            fonts = new HashMap<String, PDFont>();
            fonts.put("F1", font);

//            contentStream.setFont(font, 12);
            contentStream.setFont(font, 12);
            contentStream.beginText();
            contentStream.moveTextPositionByAmount(100, 700);
            contentStream.drawString("DIGITAL SIGNATURE TEST");
            contentStream.endText();
            contentStream.close();
            document.addPage(page);

//To Affix Visible Digital Signature
            PDAcroForm acroForm = new PDAcroForm(document);
            catalog.setAcroForm(acroForm);

            PDSignatureField sf = new PDSignatureField(acroForm);

            PDSignature pdSignature = new PDSignature();
            page.getAnnotations().add(sf.getWidget());
            pdSignature.setName("sign");
            pdSignature.setByteRange(new int[]{0, 0, 0, 0});
            pdSignature.setContents(new byte[4 * 1024]);
            pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            pdSignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
            pdSignature.setName("NAME");
            pdSignature.setLocation("LOCATION");
            pdSignature.setReason("SECURITY");
            pdSignature.setSignDate(Calendar.getInstance());
            List<PDField> acroFormFields = acroForm.getFields();

            sf.setSignature(pdSignature);
            sf.getWidget().setPage(page);

            COSDictionary acroFormDict = acroForm.getDictionary();
            acroFormDict.setDirect(true);
            acroFormDict.setInt(COSName.SIG_FLAGS, 3);
            acroFormFields.add(sf);

            PDRectangle frmRect = new PDRectangle();
//            float[] frmRectParams = {lowerLeftX,lowerLeftY,upperRightX,upperRight};
//            float[] frmRectLowerLeftUpperRightCoordinates = {5f, page.getMediaBox().getHeight() - 50f, 100f, page.getMediaBox().getHeight() - 5f};
            float[] frmRectLowerLeftUpperRightCoordinates = {5f, 5f, 205f, 55f};
            frmRect.setUpperRightX(frmRectLowerLeftUpperRightCoordinates[2]);
            frmRect.setUpperRightY(frmRectLowerLeftUpperRightCoordinates[3]);
            frmRect.setLowerLeftX(frmRectLowerLeftUpperRightCoordinates[0]);
            frmRect.setLowerLeftY(frmRectLowerLeftUpperRightCoordinates[1]);

            sf.getWidget().setRectangle(frmRect);

            COSArray procSetArr = new COSArray();
            procSetArr.add(COSName.getPDFName("PDF"));
            procSetArr.add(COSName.getPDFName("Text"));
            procSetArr.add(COSName.getPDFName("ImageB"));
            procSetArr.add(COSName.getPDFName("ImageC"));
            procSetArr.add(COSName.getPDFName("ImageI"));

            String signImageFilePath = "resources/sign.JPG";
            File signImageFile = new File(signImageFilePath);
            InputStream signImageStream = new FileInputStream(signImageFile);
            PDJpeg img = new PDJpeg(document, signImageStream);

            PDResources holderFormResources = new PDResources();
            PDStream holderFormStream = new PDStream(document);
            PDXObjectForm holderForm = new PDXObjectForm(holderFormStream);
            holderForm.setResources(holderFormResources);
            holderForm.setBBox(frmRect);
            holderForm.setFormType(1);

            PDAppearanceDictionary appearance = new PDAppearanceDictionary();
            appearance.getCOSObject().setDirect(true);
            PDAppearanceStream appearanceStream = new PDAppearanceStream(holderForm.getCOSStream());
            appearance.setNormalAppearance(appearanceStream);
            sf.getWidget().setAppearance(appearance);
            acroFormDict.setItem(COSName.DR, holderFormResources.getCOSDictionary());

            PDResources innerFormResources = new PDResources();
            PDStream innerFormStream = new PDStream(document);
            PDXObjectForm innerForm = new PDXObjectForm(innerFormStream);
            innerForm.setResources(innerFormResources);
            innerForm.setBBox(frmRect);
            innerForm.setFormType(1);

            String innerFormName = holderFormResources.addXObject(innerForm, "FRM");

            PDResources imageFormResources = new PDResources();
            PDStream imageFormStream = new PDStream(document);
            PDXObjectForm imageForm = new PDXObjectForm(imageFormStream);
            imageForm.setResources(imageFormResources);
            byte[] AffineTransformParams = {1, 0, 0, 1, 0, 0};
            AffineTransform affineTransform = new AffineTransform(AffineTransformParams[0], AffineTransformParams[1], AffineTransformParams[2], AffineTransformParams[3], AffineTransformParams[4], AffineTransformParams[5]);
            imageForm.setMatrix(affineTransform);
            imageForm.setBBox(frmRect);
            imageForm.setFormType(1);

            String imageFormName = innerFormResources.addXObject(imageForm, "n");
            String imageName = imageFormResources.addXObject(img, "img");

            innerForm.getResources().getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);
            page.getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);
            innerFormResources.getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);
            imageFormResources.getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);
            holderFormResources.getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);

            String holderFormComment = "q 1 0 0 1 0 0 cm /" + innerFormName + " Do Q \n";
            String innerFormComment = "q 1 0 0 1 0 0 cm /" + imageFormName + " Do Q\n";
            String imgFormComment = "q " + 100 + " 0 0 50 0 0 cm /" + imageName + " Do Q\n";

            appendRawCommands(holderFormStream.createOutputStream(), holderFormComment);
            appendRawCommands(innerFormStream.createOutputStream(), innerFormComment);
            appendRawCommands(imageFormStream.createOutputStream(), imgFormComment);

            documentOutputStream = new ByteArrayOutputStream();
            document.save(documentOutputStream);
            document.close();
            tempSignedPdfFileOutputStream.write(documentOutputStream.toByteArray());
            generateSignedPdf();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void appendRawCommands(OutputStream os, String commands) throws IOException {
        os.write(commands.getBytes("ISO-8859-1"));
        os.close();
    }

    public void generateSignedPdf() {
        try {
            //Find the Initial Byte Range Offsets
            String docString = new String(documentOutputStream.toByteArray(), "ISO-8859-1");
            offsetContentStart = (documentOutputStream.toString().indexOf("Contents <") + 10 - 1);
            offsetContentEnd = (documentOutputStream.toString().indexOf("000000>") + 7);
            secondPartLength = (documentOutputStream.size() - documentOutputStream.toString().indexOf("000000>") - 7);
            //Calculate the Updated ByteRange
            String initByteRange = "";
            if (docString.indexOf("/ByteRange [0 1000000000 1000000000 1000000000]") > 0) {
                initByteRange = "/ByteRange [0 1000000000 1000000000 1000000000]";
            } else if (docString.indexOf("/ByteRange [0 0 0 0]") > 0) {
                initByteRange = "/ByteRange [0 0 0 0]";
            } else {
                System.out.println("No /ByteRange Token is Found!");
                System.exit(1);
            }

            String interimByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";
            int byteRangeLengthDifference = interimByteRange.length() - initByteRange.length();
            offsetContentStart = offsetContentStart + byteRangeLengthDifference;
            offsetContentEnd = offsetContentEnd + byteRangeLengthDifference;
            String finalByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";
            byteRangeLengthDifference += interimByteRange.length() - finalByteRange.length();
            //Replace the ByteRange
            docString = docString.replace(initByteRange, finalByteRange);

            //Update xref Table
            int xrefOffset = docString.indexOf("xref");
            int startObjOffset = docString.indexOf("0000000000 65535 f") + "0000000000 65535 f".length() + 1;
            int trailerOffset = docString.indexOf("trailer") - 2;
            String initialXrefTable = docString.substring(startObjOffset, trailerOffset);
            int signObjectOffset = docString.indexOf("/Type /Sig") - 3;
            String updatedXrefTable = "";
            while (initialXrefTable.indexOf("n") > 0) {
                String currObjectRefEntry = initialXrefTable.substring(0, initialXrefTable.indexOf("n") + 1);
                String currObjectRef = currObjectRefEntry.substring(0, currObjectRefEntry.indexOf(" 00000 n"));
                int currObjectOffset = Integer.parseInt(currObjectRef.trim().replaceFirst("^0+(?!$)", ""));
                if ((currObjectOffset + byteRangeLengthDifference) > signObjectOffset) {
                    currObjectOffset += byteRangeLengthDifference;
                    int currObjectOffsetDigitsCount = Integer.toString(currObjectOffset).length();
                    currObjectRefEntry = currObjectRefEntry.replace(currObjectRefEntry.substring(currObjectRef.length() - currObjectOffsetDigitsCount, currObjectRef.length()), Integer.toString(currObjectOffset));
                    updatedXrefTable += currObjectRefEntry;
                } else {
                    updatedXrefTable += currObjectRefEntry;
                }
                initialXrefTable = initialXrefTable.substring(initialXrefTable.indexOf("n") + 1);
            }
            //Replace with Updated xref Table
            docString = docString.substring(0, startObjOffset).concat(updatedXrefTable).concat(docString.substring(trailerOffset));

            //Update startxref
            int startxrefOffset = docString.indexOf("startxref");
            //Replace with Updated startxref
            docString = docString.substring(0, startxrefOffset).concat("startxref\n".concat(Integer.toString(xrefOffset))).concat("\n%%EOF\n");

            //Construct Original Document For Signature by Removing Temporary Enveloped Detached Signed Content(000...000)
            contentString = docString.substring(offsetContentStart + 1, offsetContentEnd - 1);
            String docFirstPart = docString.substring(0, offsetContentStart);
            String docSecondPart = docString.substring(offsetContentEnd);
            String docForSign = docFirstPart.concat(docSecondPart);

            //Generate Signature
            pdfFileOutputStream.write(docForSign.getBytes("ISO-8859-1"));
            File keyStorefile = new File(storePath);
            InputStream keyStoreInputStream = new FileInputStream(keyStorefile);
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(keyStoreInputStream, keyStorePassword.toCharArray());
            certChain = keyStore.getCertificateChain(entryAlias);
            PrivateKey privateKey = (PrivateKey) keyStore.getKey(entryAlias, keyStorePassword.toCharArray());
            List<Certificate> certList = new ArrayList<Certificate>();
            certList = Arrays.asList(certChain);
            Store store = new JcaCertStore(certList);
//            String algorithm="SHA1WithRSA";
//            String algorithm="SHA2WithRSA";
            String algorithm = "MD5WithRSA";
            //String algorithm = "DSA";

            //Updated Sign Method
            CMSTypedData msg = new CMSProcessableByteArray(docForSign.getBytes("ISO-8859-1"));
            CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
            /* Build the SignerInfo generator builder, that will build the generator... that will generate the SignerInformation... */
            SignerInfoGeneratorBuilder signerInfoBuilder = new SignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider(BC).build());
            //JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder("SHA2withRSA");
            JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder(algorithm);
            contentSigner.setProvider(BC);
            SignerInfoGenerator signerInfoGenerator = signerInfoBuilder.build(contentSigner.build(privateKey), new X509CertificateHolder(certList.get(0).getEncoded()));
            generator.addSignerInfoGenerator(signerInfoGenerator);
            generator.addCertificates(store);
            CMSSignedData signedData = generator.generate(msg, false);
            String apHexEnvelopedData = org.apache.commons.codec.binary.Hex.encodeHexString(signedData.getEncoded()).toUpperCase();
            //Construct Content Tag Data
            contentString = apHexEnvelopedData.concat(contentString).substring(0, contentString.length());
            contentString = "<".concat(contentString).concat(">");
            //Construct Signed Document
            String signedDoc = docFirstPart.concat(contentString).concat(docSecondPart);
            //Write Signed Document to File
            signedPdfFileOutputStream.write(signedDoc.getBytes("ISO-8859-1"));
            signedPdfFileOutputStream.close();
            signedDoc = null;
        } catch (Exception e) {
            throw new RuntimeException("Error While Generating Signed Data", e);
        }
    }

    public static void main(String[] args) {
        AffixSignature affixSignature = new AffixSignature();
    }
}

Под умелым руководством MKL теперь обновленный код подписывает вновь созданный документ. Спасибо MKL!


person Community    schedule 12.03.2014    source источник
comment
Создается какой-то подписанный PDF-документ, но есть ошибки - поделитесь этим документом. Слишком много кода, чтобы что-то сказать, не затрачивая значительного количества времени, но просмотр образца вывода может дать немедленное представление о том, что искать.   -  person mkl    schedule 13.03.2014
comment
Я быстро просмотрел ваш код и сомневаюсь, что PDFBox будет доволен вашими appendRawCommands(XXXFormStream.createOutputStream(), YYY) вызовами (создание выходных потоков для одной и той же формы более одного раза может быть проблемой, а также переключение между формами); кроме того, похоже, что между несколькими строками, которые вы пытаетесь записать в один и тот же поток, нет пробелов, что приводит к неизвестным операторам Qq. Кроме того, appendRawCommands использует UTF-8, который полностью не является PDF.   -  person mkl    schedule 14.03.2014
comment
Ваш generateSignedDocument, вероятно, причинит довольно много вреда, поскольку предполагает, что может работать с PDF-файлами, как если бы они были текстовыми файлами. В общем, это не так. И сами манипуляции тоже выглядят очень сомнительно.   -  person mkl    schedule 14.03.2014
comment
Пожалуйста, поделитесь PDF-файлом в виде бинарной загрузки где-нибудь (например, в общедоступной папке Dropbox); есть некоторые детали, которые просто теряются при публикации PDF-файлов в виде текста. При этом структура выглядит вполне разумно, просто поток 8 0 ссылается на себя в своих ресурсах XObject, что может помочь любому просмотрщику pdf. И вы, кажется, отредактировали больше, чем хотели, в вашем посте нет объектов 11 0 и 12 0.   -  person mkl    schedule 17.03.2014
comment
Если вы сравните байты обоих документов, вы обнаружите, что есть много нежелательных изменений, на первый взгляд особенно замена определенных байтов знаками вопроса. Скорее всего, это связано с тем, что вы сделали byte[] a String для облегчения работы с документом и, в конечном итоге, снова изменили его на byte[]. Быстрый тест показывает, что (в случае латинского кодирования) эта процедура изменяет байты 0x81, 0x8d, 0x8f, 0x90 и 0x9d на 0x3f (то есть вопросительный знак). Таким образом, здесь вам придется работать с операциями byte и byte[] вместо операций String.   -  person mkl    schedule 17.03.2014
comment
Хорошо, я добавил ответ до того, как увидел это изменение. Да, ISO-8859-1, похоже, не разбивает какие-либо конкретные байты, как по умолчанию, но я все же сомневаюсь, что это хорошая идея. Замечания относительно ошибок в контейнере подписи и его конструкции остаются в силе.   -  person mkl    schedule 18.03.2014
comment
Вы все еще используете generator.generate(msg, true);, почему вы используете здесь true?   -  person mkl    schedule 19.03.2014
comment
Хорошо, теперь это false. Каков текущий выход? Принимает ли Adobe Reader сейчас? В противном случае обновите образец PDF-файла.   -  person mkl    schedule 19.03.2014
comment
Я добавил два раздела к своему ответу ниже. Наиболее важной проблемой для вас сейчас, скорее всего, является проблема вычисления значения хэша.   -  person mkl    schedule 19.03.2014
comment
Как добавить текст подписи в видимую подпись на слое 2. - по сути, вам нужно зарегистрировать шрифт (например, с именем F0) в imageFormResources и добавить что-то вроде " BT /F0 12 Tf 3 3 Td (Text) Tj ET " в imgFormComment .   -  person mkl    schedule 21.03.2014


Ответы (1)


Хотя изначально эти подсказки были представлены как комментарии к исходному вопросу, теперь их стоит сформулировать как ответ:

Проблемы с кодом

Хотя кода слишком много, чтобы просмотреть и исправить, не тратя много времени, и хотя исходное отсутствие образца PDF было помехой, быстрое сканирование кода выявило некоторые проблемы:

  • Вызовы appendRawCommands(XXXFormStream.createOutputStream(), YYY) вполне вероятно вызывают проблемы с PDFBox: создание выходных потоков для одной и той же формы более одного раза может быть проблемой, а также переключение между формами.

  • Кроме того, похоже, что между несколькими строками, записанными в один и тот же поток, нет пробелов, что приводит к неизвестным операторам Qq. Кроме того, метод appendRawCommands использует кодировку UTF-8, которая является чужой для PDF.

  • generateSignedDocument, скорее всего, причинит довольно много вреда, поскольку предполагает, что может работать с PDF-файлами, как если бы они были текстовыми файлами. В общем, это не так.

Результат проблем с PDF

Примерный PDF-файл с результатами, предоставленный OP, позволяет выявить некоторые реально реализованные проблемы:

  • Сравнивая байты обоих документов (Report_08_05_23.pdf и Signed_Report_08_05_23.pdf), обнаруживается много нежелательных изменений, на первый взгляд особенно замена некоторых байтов знаками вопроса. Это связано с тем, что ByteArrayOutputStream.toString() используется для упрощения работы с документом и, в конечном итоге, превращается обратно в byte[].

    Например. ср. документы JavaDoc из ByteArrayOutputStream.toString()

    * <p> This method always replaces malformed-input and unmappable-character
    * sequences with the default replacement string for the platform's
    * default character set. The {@linkplain java.nio.charset.CharsetDecoder}
    * class should be used when more control over the decoding process is
    * required.
    

    Некоторые значения байтов не представляют символы в наборе символов по умолчанию для платформы и поэтому преобразуются в Unicode заменяющий символ и при окончательном преобразовании в byte[] становится 0x3f (код ASCII для вопросительного знака). Это изменение уничтожает сжатое содержимое потока, как потоков содержимого, так и потоков изображений.

    Чтобы исправить это, нужно работать с операциями byte и byte[] вместо операций String здесь.

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

Проблемы с контейнером подписи

Подпись не проверяет. Таким образом, он также пересматривается.

  • Осмотрев контейнер подписи, можно увидеть, что он неправильный: несмотря на то, что подпись является adbe.pkcs7.detached, контейнер подписи встраивает данные. Глядя на код, причина становится ясной:

    CMSSignedData sigData = generator.generate(msg, true);
    

    Параметр true просит BC встроить данные msg.

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

    Signature signature = Signature.getInstance(algorithm, BC);
    signature.initSign(privateKey);
    signature.update(docForSign.getBytes());
    CMSTypedData msg = new CMSProcessableByteArray(signature.sign());
    

что неверно, поскольку созданный позже SignerInfoGenerator используется для создания фактической подписи.

Изменить: после того, как проблемы, упомянутые ранее, были исправлены или, по крайней мере, обойдены, подпись по-прежнему не принимается Adobe Reader. Итак, еще раз взглянем на код и:

Проблема с вычислением значения хэша

OP создает это значение ByteRange.

String finalByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";

и более поздние наборы

String docFirstPart = docString.substring(0, offsetContentStart + 1);
String docSecondPart = docString.substring(offsetContentEnd - 1);

+ 1 и - 1 предназначены для включения в эти части документа также < и > байтов подписи. Но OP также использует эти строки для создания подписанных данных:

String docForSign = docFirstPart.concat(docSecondPart);

Это неверно, байты со знаком не содержат < и >. Таким образом, значение хеш-функции, вычисленное позже, также неверно, и у Adobe Reader есть веские основания предполагать, что документ был изменен.

При этом время от времени могут возникать и другие проблемы:

Проблемы с обновлением смещения и длины

OP вставляет диапазон байтов следующим образом:

String interimByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";
int byteRangeLengthDifference = interimByteRange.length() - initByteRange.length();
offsetContentStart = offsetContentStart + byteRangeLengthDifference;
offsetContentEnd = offsetContentEnd + byteRangeLengthDifference;
String finalByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";
byteRangeLengthDifference += interimByteRange.length() - finalByteRange.length();
//Replace the ByteRange
docString = docString.replace(initByteRange, finalByteRange);

Время от времени offsetContentStart или offsetContentEnd будет немного ниже 10 ^ n, а потом немного выше. Линия

byteRangeLengthDifference += interimByteRange.length() - finalByteRange.length();

пытается это исправить, но finalByteRange (который в конечном итоге вставляется в документ) по-прежнему содержит неисправленные значения.

Аналогичным образом представление начала внешней ссылки вставлено следующим образом

docString = docString.substring(0, startxrefOffset).concat("startxref\n".concat(Integer.toString(xrefOffset))).concat("\n%%EOF\n");

также может быть длиннее, чем раньше, из-за чего диапазон байтов (рассчитанный заранее) не покрывает весь документ.

Кроме того, поиск смещений соответствующих объектов PDF с помощью текстового поиска по всему документу.

offsetContentStart = (documentOutputStream.toString().indexOf("Contents <") + 10 - 1);
offsetContentEnd = (documentOutputStream.toString().indexOf("000000>") + 7);
...
int xrefOffset = docString.indexOf("xref");
...
int startxrefOffset = docString.indexOf("startxref");

не будет работать для общих документов. Например. если в документе уже есть предыдущие подписи, вполне вероятно, что так будут идентифицированы неправильные индексы.

person mkl    schedule 18.03.2014