【问题标题】:Attachment damages signature part 2附件损坏签名第 2 部分
【发布时间】:2015-05-30 18:27:20
【问题描述】:

我创建了将图像添加到现有 pdf 文档然后对其进行签名的代码,全部使用 PDFBox(请参见下面的代码)。

代码很好地添加了图像和签名。但是,在某些文档中,Acrobat Reader 会抱怨“签名字节范围无效。”

问题似乎与this问题中描述的问题相同。该问题的答案更详细地描述了该问题:问题是我的代码在文档中留下了交叉引用类型(流和表)的混合。实际上,由于这会产生问题,某些文档甚至无法打开。

我的问题是:如何防止这种情况发生?如何在不创建多个交叉引用类型的情况下将图像添加到现有 pdf 文档?

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

【问题讨论】:

  • 当您开始操作原始 PDF 并再次保存它(不是作为增量更新)时,应该可以以与 pdfbox 签名增量更新兼容的方式进行保存。明天我会试着调查一下。
  • 哇,这很奇怪。我刚刚做了一些测试,结果是对于带有外部参照流的 PDF,PDFBox 使用外部参照表保存第一部分(添加了图形的部分),并使用外部参照流附加签名修订。我会再调查一下。
  • 非常感谢您的帮助。我不是很懂PDF。您是否使用任何特定工具来检查 PDF,还是仅使用十六进制编辑器?
  • 为了识别交叉引用的类型,我实际上只是使用了一个文本查看器。

标签: java digital-signature pdfbox attachment


【解决方案1】:

问题

正如this answer 中已经解释的那样,这里的问题是

  • 当使用添加的图像以非增量方式存储文档时,PDFBox 1.8.9 使用交叉引用表执行此操作,无论原始文件使用表还是流;如果原始文件使用流,则将交叉引用流字典条目复制到 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
    

    (这里的大部分预告片条目都是无用的,甚至是误导性的,见下文。)

  • 当增量保存签名时,COSWriter.doWriteXRefInc 使用COSDocument.isXRefStream 来确定现有文档(我们上面存储的那个)是否使用交叉引用流。如上所述,它没有。不幸的是,PDFBox 1.8.9 中的COSDocument.isXRefStream 被实现为

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

    因此,上面显示的误导性 trailer 条目 Type 使 PDFBox 认为它必须使用交叉引用流。

结果是一个文档,其初始修订以交叉引用表和奇怪的预告片条目结尾,其第二个修订以交叉引用流结尾。这是无效的。

一种解决方法

不过,幸运的是,了解问题产生的原因提供了一种解决方法:删除麻烦的 trailer 条目,例如像这样:

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

通过此解决方法,已签名文档中的两个修订都使用交叉引用表,并且签名有效。

请注意,如果即将发布的 PDFBox 版本更改为使用外部参照流保存从具有交叉引用流的源加载的文档,则必须再次删除解决方法。

不过,我认为这不会在未来的 1.x.x 版本中发生,而 2.0.0 版将引入一个根本改变的 API,因此原始代码不会开箱即用那么无论如何。


其他想法

我也尝试了其他方法来规避这个问题,试图

  • 也将第一次操作存储为增量更新,或者
  • 在与签名相同的增量更新期间添加图像,

参见。 SignLikeUnOriginalToo.java,但失败了。 PDFBox 1.8.9 增量更新似乎只适用于添加签名。


重新审视其他想法

在研究了更多使用 PDFBox 创建附加修订后,我再次尝试了其他想法,现在成功了!

关键部分是将添加和更改的对象标记为已更新,包括文档目录中的路径。

应用第一个想法(将图像添加为显式的中间修订版)相当于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();
...

(如SignLikeUnOriginalToo.java方法doSignTwoRevisions

应用第二个想法(添加图像作为签名修订的一部分)相当于 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();
...

(如SignLikeUnOriginalToo.java方法doSignOneStep

这两种变体显然都优于原始方法。

【讨论】:

  • 这完全有效!惊人的!非常感谢所有帮助(考虑到您的 cmets 花了您相当长的时间)和详尽的答案。也为之前在线程中提出问题而道歉。我更习惯于“常规”论坛,您只需在其中继续一个线程,但我知道这不是 StackOverflow 的工作方式并理解它的优雅。
  • 谢谢!多么好的解释,彻底的答案。
猜你喜欢
  • 2014-02-11
  • 2022-01-01
  • 1970-01-01
  • 2022-01-14
  • 1970-01-01
  • 2021-06-09
  • 2014-11-04
  • 2014-05-09
  • 2012-03-16
相关资源
最近更新 更多