How to use pdfbox to digitally sign dynamically created PDF documents?

I'm sorry! I'm poor in Java Please correct me where I am wrong and improve where I am poor!

I'm trying to use PDF@R_778_2419 @Digitally sign dynamically created PDFs using the following procedure:

The tasks of the plan: (I) create a template PDF (II) update byterange, XRef, startxref (III) create and build an original file for the signature (IV) create a separate envelope digital signature (V) build a digitally signed PDF document by connecting the original document part – I, independent signature and original PDF part – II

Observation: (I) pdffileoutputstream write(documentOutputStream.toByteArray()); Create a template PDF document using visible signature

(II) it creates some PDF signed documents with errors (a) invalid tokens and (b) several parser errors (now corrected under the guidance of MKL's capabilities!)

Please suggest the following:

(i) How to add signature text to visible signature on layer2

Thank you in advance!

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.pdf@R_778_2419@.cos.COSArray;
    import org.apache.pdf@R_778_2419@.cos.COSDictionary;
    import org.apache.pdf@R_778_2419@.cos.COSName;
    import org.apache.pdf@R_778_2419@.pdmodel.PDDocument;
    import org.apache.pdf@R_778_2419@.pdmodel.PDPage;
    import org.apache.pdf@R_778_2419@.pdmodel.PDResources;
    import org.apache.pdf@R_778_2419@.pdmodel.common.PDRectangle;
    import org.apache.pdf@R_778_2419@.pdmodel.common.PDStream;
    import org.apache.pdf@R_778_2419@.pdmodel.edit.PDPageContentStream;
    import org.apache.pdf@R_778_2419@.pdmodel.font.PDFont;
    import org.apache.pdf@R_778_2419@.pdmodel.font.PDType1Font;
    import org.apache.pdf@R_778_2419@.pdmodel.graphics.xobject.PDJpeg;
    import org.apache.pdf@R_778_2419@.pdmodel.graphics.xobject.PDXObjectForm;
    import org.apache.pdf@R_778_2419@.pdmodel.interactive.annotation.PDAppearanceDictionary;
    import org.apache.pdf@R_778_2419@.pdmodel.interactive.annotation.PDAppearanceStream;
    import org.apache.pdf@R_778_2419@.pdmodel.interactive.digitalsignature.PDSignature;
    import org.apache.pdf@R_778_2419@.pdmodel.interactive.digitalsignature.SignatureOptions;
    import org.apache.pdf@R_778_2419@.pdmodel.interactive.form.PDAcroForm;
    import org.apache.pdf@R_778_2419@.pdmodel.interactive.form.PDField;
    import org.apache.pdf@R_778_2419@.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});
            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.getMedia@R_778_2419@().getHeight() - 50f,100f,page.getMedia@R_778_2419@().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.setB@R_778_2419@(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.setB@R_778_2419@(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,1,0};
            AffineTransform affineTransform = new AffineTransform(AffineTransformParams[0],AffineTransformParams[1],AffineTransformParams[2],AffineTransformParams[3],AffineTransformParams[4],AffineTransformParams[5]);
            imageForm.setMatrix(affineTransform);
            imageForm.setB@R_778_2419@(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();
    }
}

Under the guidance of MKL's ability, the updated code now signs the newly created document Thank you MKL!

Solution

Although these tips were originally presented as comments on the original question, they are now worth formulating as answers:

Code problem

Although too much code needs to be reviewed and repaired without spending a lot of time, and although the initial lack of sample PDF is an obstacle, a quick scan of the code will find some problems:

>The appendrawcommands (xxxforms stream. Createoutputstream(), YYY) call is likely to cause PDF@R_778_2419 @There is a problem: creating the output stream of the same form multiple times may be a problem, and switching back and forth between forms. > In addition, there seems to be no space between multiple strings written to the same class, resulting in an unknown QQ operator In addition, the appendrawcommands method uses UTF-8, which is strange to PDF. > Generatesigned document is most likely to cause great damage because it assumes that it can process text documents like PDF This is not generally the case

Results PDF questions

Finally, the sample result PDF provided by the op can identify some actual implemented problems:

>Comparing the bytes of the two documents (report_08_05_23. PDF and signed_report_08_05_23. PDF), we find that there are many unnecessary changes. At first glance, especially replacing some bytes with question marks This is because bytearrayoutputstream ToString () can easily manipulate the document and eventually change it back to byte []

For example Compare bytearrayoutputstream JavaDocs for 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.

Some byte values do not represent characters in the platform default character set, so they are converted to unicode replacement character, and finally converted to byte [] to 0x3f (ASCII code of question mark) This change will kill the compressed content of the content stream and the image stream

To solve this problem, you must use byte and byte [] operations instead of string operations. > Stream 8 0 references itself in its xobject resource, which may cause any PDF viewer to throw Please don't do this cycle

Signature container problem

The signature could not be verified Therefore, it is also reviewed

>Checking the signature container shows that it is wrong: even though the signature is ADBE pkcs7. Detached, the signature container embeds data The reason for looking at the code is obvious:

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

The true parameter requires the BC to embed MSG data. > After starting to view the signature code, another problem becomes visible: the MSG data above is not just a summary, they are already signatures:

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

This is an error because the signerinfo generator created later is used to create the actual signature

Edit: adobe reader still does not accept signatures after the previously mentioned problems have been fixed or at least resolved So let's look at the code and:

Hash value calculation problem

OP constructs this byterange value

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

Set later

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

1 and - 1 are intended so that these document sections also include < 1 And > contain signature bytes However, Op also uses these strings to construct signature data:

String docForSign = docFirstPart.concat(docSecondPart);

This is an error. Signed bytes do not contain < and > Therefore, the hash value calculated later is also wrong, and adobe reader has good reason to assume that the document has been manipulated

Having said that, there are other problems at regular intervals:

Offset and length update issues

OP insert byte range is as follows:

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

Each of offsetcontentstart or offsetcontentend will be slightly lower than a certain 10 ^ n over a period of time and will be slightly higher later This line

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

Trying to make up for this, the finalbyterange (eventually inserted into the document) still contains uncorrected values

In a similar manner, the representation of the XRef begins to be inserted like this

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

It may also be longer than before, so that the byte range (pre calculated) does not cover the entire document

In addition, use the text search of the entire document to find the offset of the relevant PDF object

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

The generic file will fail For example If there is a previous signature in the document, it is likely to identify the wrong index like this

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>