Attachment damages signature part 2

二次信任 提交于 2019-12-05 11:03:24
mkl

The issue

As had already been explained in this answer, the issue at work here is that

  • when non-incrementally storing the document with the added image, PDFBox 1.8.9 does so using a cross reference table no matter if the original file used a table or stream; if the original file used a stream, the cross reference stream dictionary entries are copied into the trailer dictionary;

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

  • when incrementally saving the signature, COSWriter.doWriteXRefInc uses COSDocument.isXRefStream to determine whether the existing document (the one we stored as above) uses a cross reference stream. As mentioned above, it does not. Unfortunately, though, 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;
    }
    

    Thus, the misleading trailer entry Type shown above make PDFBox think it has to use a cross reference stream.

The result is a document whose initial revision ends with a cross reference table and weird trailer entries and whose second revision ends with a cross reference stream. This is not valid.

A work-around

Fortunately, though, understanding how the issue arises presents a work-around: Removing the troublesome trailer entry, e.g. like this:

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

With this work-around both revisions in the signed document use cross reference tables and the signature is valid.

Beware, if upcoming PDFBox versions change to save documents loaded from sources with cross reference streams using xref streams, too, the work-around must again be removed.

I would assume, though, that won't happen in the 1.x.x versions to come, and version 2.0.0 will introduce a fundamentally changed API, so the original code won't work out-of-the-box then anyhow.


Other ideas

I tried other ways, too, to circumvent this problem, trying to

  • store the first manipulation as incremental update, too, or
  • add the image during the same incremental update as the signature,

cf. SignLikeUnOriginalToo.java, but failed. PDFBox 1.8.9 incremental updates only seem to properly work for adding signatures.


Other ideas revisited

After looking into the creation of additional revisions using PDFBox some more, I tried the other ideas again and now succeeded!

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

Applying the first idea (adding the image as an explicit intermediate revision) amounts to 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 the image as part of the signing revision) amounts to 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 variants are clearly preferable to the original approach.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!