Signature Signatures Part 2

I created code that adds an image to an existing PDF document and then signs it using the PDFBox (see code below).

The code nicely adds an image and signature. However, in some documents, Acrobat Reader complains that "The signature byte range is invalid."

The problem seems to be the same as the problem described in this question. The answer to this question describes the problem in more detail: the problem is that my code leaves mixed types of links in the document (streams and tables). Indeed, some documents will not even open due to the problems this creates.

My question is: how can I prevent this? How to add an image to an existing pdf document without creating several types of cross-references?

public class TC3 implements SignatureInterface{ private char[] pin = "123456".toCharArray(); private BouncyCastleProvider provider = new BouncyCastleProvider(); private PrivateKey privKey; private Certificate[] cert; public TC3() throws Exception{ Security.addProvider(provider); KeyStore keystore = KeyStore.getInstance("PKCS12", provider); keystore.load(new FileInputStream(new File("resources/IIS_keystore.pfx")), pin.clone()); String alias = keystore.aliases().nextElement(); privKey = (PrivateKey) keystore.getKey(alias, pin); cert = keystore.getCertificateChain(alias); } public void doSign() throws Exception{ byte inputBytes[] = IOUtils.toByteArray(new FileInputStream("resources/rooster.pdf")); PDDocument pdDocument = PDDocument.load(new ByteArrayInputStream(inputBytes)); PDJpeg ximage = new PDJpeg(pdDocument, ImageIO.read(new File("resources/logo.jpg"))); PDPage page = (PDPage)pdDocument.getDocumentCatalog().getAllPages().get(0); PDPageContentStream contentStream = new PDPageContentStream(pdDocument, page, true, true); contentStream.drawXObject(ximage, 50, 50, 356, 40); contentStream.close(); ByteArrayOutputStream os = new ByteArrayOutputStream(); pdDocument.save(os); os.flush(); pdDocument.close(); inputBytes = os.toByteArray(); pdDocument = PDDocument.load(new ByteArrayInputStream(inputBytes)); PDSignature signature = new PDSignature(); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); signature.setName("signer name"); signature.setLocation("signer location"); signature.setReason("reason for signature"); signature.setSignDate(Calendar.getInstance()); pdDocument.addSignature(signature, this); File outputDocument = new File("resources/signed.pdf"); ByteArrayInputStream fis = new ByteArrayInputStream(inputBytes); FileOutputStream fos = new FileOutputStream(outputDocument); byte[] buffer = new byte[8 * 1024]; int c; while ((c = fis.read(buffer)) != -1) { fos.write(buffer, 0, c); } fis.close(); FileInputStream is = new FileInputStream(outputDocument); pdDocument.saveIncremental(is, fos); pdDocument.close(); } public byte[] sign(InputStream content) { CMSProcessableInputStream input = new CMSProcessableInputStream(content); CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); List<Certificate> certList = Arrays.asList(cert); CertStore certStore = null; try{ certStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(certList), provider); gen.addSigner(privKey, (X509Certificate) certList.get(0), CMSSignedGenerator.DIGEST_SHA256); gen.addCertificatesAndCRLs(certStore); CMSSignedData signedData = gen.generate(input, false, provider); return signedData.getEncoded(); }catch (Exception e){} return null; } public static void main(String[] args) throws Exception { new TC3().doSign(); } 
+3
source share
1 answer

Problem

As already explained in this answer , the problem here is that

  • when non-incremental storage of a document with an added image, PDFBox 1.8.9 does this using a cross-reference table, regardless of whether the source file used a table or stream; if the source file used the stream, the entries in the dictionaries of the cross reference stream are copied to the dictionary trailer ;

     ... 0000033667 00000 n 0000033731 00000 n trailer << /DecodeParms << /Columns 4 /Predictor 12 >> /Filter /FlateDecode /ID [<5BD95916CAE5E84E9D964396022CBDCD> <6420B4547602C943AF37DD6C77496BE8>] /Info 6 0 R /Length 61 /Root 1 0 R /Size 35 /Type /XRef /W [1 2 1] /Index [20 22] >> startxref 35917 %%EOF 

    (Most of these trailer entries here are useless or even misleading, see below.)

  • while gradually saving the signature, COSWriter.doWriteXRefInc uses COSDocument.isXRefStream to determine if the existing document (the one we saved as described above) is used for cross-reference flow. As mentioned above, this is not the case. Unfortunately, however, COSDocument.isXRefStream in PDFBox 1.8.9 is implemented as

     public boolean isXRefStream() { if (trailer != null) { return COSName.XREF.equals(trailer.getItem(COSName.TYPE)); } return false; } 

    Therefore, the misleading record trailer Type made in the PDFBox suggests that it should use a cross-reference stream.

The result is a document whose initial version ends with a cross-reference table and strange trailer entries and the second version of which ends with a cross reference stream. This is not true.

Bypass

However, fortunately, understanding how the problem arises is a workaround: deleting a difficult recording trailer , for example. eg:

  inputBytes = os.toByteArray(); pdDocument = PDDocument.load(new ByteArrayInputStream(inputBytes)); pdDocument.getDocument().getTrailer().removeItem(COSName.TYPE); // <<<<<<<<<< Remove misleading entry <<<<<<<<<< 

As part of this work, both versions in the signed document use cross-reference tables, and the signature is valid.

Beware if upcoming versions of PDFBox change to preserve documents downloaded from sources using cross-references using xref streams, you also need to delete the crawl again.

I would suggest that this does not happen in versions 1.xx, and version 2.0.0 will introduce a fundamentally changed API, so the source code will not work out of the box, anyway.


Other ideas

I tried other ways to get around this problem by trying

  • save the first manipulation as an incremental update, or
  • add the image during the same incremental update as the signature,

Wed SignLikeUnOriginalToo.java but failed. It seems that incremental updates to PDFBox 1.8.9 work correctly for adding signatures.


Other ideas revised

Having studied additional versions using PDFBox, I tried other ideas again and now succeeded!

The most important part is to mark the added and changed objects as updated, including the path from the document catalog.

Applying the first idea (adding an image as an explicit intermediate revision) makes up this change in doSign :

 ... FileOutputStream fos = new FileOutputStream(intermediateDocument); FileInputStream fis = new FileInputStream(intermediateDocument); byte inputBytes[] = IOUtils.toByteArray(inputStream); PDDocument pdDocument = PDDocument.load(new ByteArrayInputStream(inputBytes)); PDJpeg ximage = new PDJpeg(pdDocument, ImageIO.read(logoStream)); PDPage page = (PDPage) pdDocument.getDocumentCatalog().getAllPages().get(0); PDPageContentStream contentStream = new PDPageContentStream(pdDocument, page, true, true); contentStream.drawXObject(ximage, 50, 50, 356, 40); contentStream.close(); pdDocument.getDocumentCatalog().getCOSObject().setNeedToBeUpdate(true); pdDocument.getDocumentCatalog().getPages().getCOSObject().setNeedToBeUpdate(true); page.getCOSObject().setNeedToBeUpdate(true); page.getResources().getCOSObject().setNeedToBeUpdate(true); page.getResources().getCOSDictionary().getDictionaryObject(COSName.XOBJECT).setNeedToBeUpdate(true); ximage.getCOSObject().setNeedToBeUpdate(true); fos.write(inputBytes); pdDocument.saveIncremental(fis, fos); pdDocument.close(); pdDocument = PDDocument.load(intermediateDocument); PDSignature signature = new PDSignature(); ... 

(as in SignLikeUnOriginalToo.java method doSignTwoRevisions )

Applying the second idea (adding an image as part of a signature revision) makes up this change in doSign :

 ... byte inputBytes[] = IOUtils.toByteArray(inputStream); PDDocument pdDocument = PDDocument.load(new ByteArrayInputStream(inputBytes)); PDJpeg ximage = new PDJpeg(pdDocument, ImageIO.read(logoStream)); PDPage page = (PDPage) pdDocument.getDocumentCatalog().getAllPages().get(0); PDPageContentStream contentStream = new PDPageContentStream(pdDocument, page, true, true); contentStream.drawXObject(ximage, 50, 50, 356, 40); contentStream.close(); page.getResources().getCOSObject().setNeedToBeUpdate(true); page.getResources().getCOSDictionary().getDictionaryObject(COSName.XOBJECT).setNeedToBeUpdate(true); ximage.getCOSObject().setNeedToBeUpdate(true); PDSignature signature = new PDSignature(); ... 

(as in SignLikeUnOriginalToo.java method doSignOneStep )

Both options are clearly preferable to the initial approach.

+4
source

Source: https://habr.com/ru/post/1232694/


All Articles