In Itext 7, how to sign a pdf with 2 steps?

2019-01-29 03:27发布

问题:

Following the answers given in this previous question : In Itext 7, how to get the range stream to sign a pdf?, i've tried to reimplement the two steps signing method working in Itext 5 but i encounter an issue when trying to reopen the document result of the first step (with the PdfReader or a pdf reader).(invalid document)

Here is the presigning part for a document already containing an empty signature field named certification ... why is the result of this step invalid ?

PdfReader reader = new PdfReader(fis);
Path signfile = Files.createTempFile("sign", ".pdf");
FileOutputStream os = new FileOutputStream(signfile.toFile());
PdfSigner signer = new PdfSigner(reader, os, false);
signer.setFieldName("certification"); // this field already exists
signer.setCertificationLevel(PdfSigner.CERTIFIED_FORM_FILLING);
PdfSignatureAppearance sap = signer.getSignatureAppearance();
sap.setReason("Certification of the document");
sap.setLocation("On server");
sap.setCertificate(maincertificate);
BouncyCastleDigest digest = new BouncyCastleDigest();
PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest,false);
 //IExternalSignatureContainer like BlankContainer
PreSignatureContainer external = new    PreSignatureContainer(PdfName.Adobe_PPKLite,PdfName.Adbe_pkcs7_detached);
signer.signExternalContainer(external, 8192);
byte[] hash=external.getHash();
byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, null, null,PdfSigner.CryptoStandard.CMS);// sh will be sent for signature

And here is the PreSignatureContainer class :

public class PreSignatureContainer implements IExternalSignatureContainer {

private PdfDictionary sigDic;
private byte hash[];


public PreSignatureContainer(PdfName filter, PdfName subFilter) {

    sigDic = new PdfDictionary();
    sigDic.put(PdfName.Filter, filter);
    sigDic.put(PdfName.SubFilter, subFilter);
}

@Override
public byte[] sign(InputStream data) throws GeneralSecurityException {
    String hashAlgorithm = "SHA256";
    BouncyCastleDigest digest = new BouncyCastleDigest();

    try {
    this.hash= DigestAlgorithms.digest(data, digest.getMessageDigest(hashAlgorithm));
    } catch (IOException e) {
        throw new GeneralSecurityException("PreSignatureContainer signing exception",e);
    }

    return new byte[0];
}

@Override
public void modifySigningDictionary(PdfDictionary signDic) {
    signDic.putAll(sigDic);

}

public byte[] getHash() {
    return hash;
}

public void setHash(byte hash[]) {
    this.hash = hash;
}

}

回答1:

why is the result of this step invalid

Because you essentially discovered a bug... ;)

Your sample input file has one feature which triggers the bug: It is compressed using object streams.

When iText manipulates such a file, it also tries to put as many objects as possible into object streams. Unfortunately it also does so with the signature dictionary. This is unfortunate because after writing the whole file it tries to enter some information (which are not available before) into this dictionary which damages the compressed object stream.


What you can do...

You can either

  • wait for iText development to fix this issue - I assume this won't take too long but probably you don't have the time to wait; or
  • convert the file to sign into a form which does not use object streams - this can be done using iText itself but probably you cannot accept the file growth this means, or probably the files already are signed which forbids any such transformation; or
  • patch iText 7 to force the signature dictionary not to be added to an object stream - it is a trivial patch but you probably don't want to used patched libraries.

The patch mentioned above indeed is trivial, the method PdfSigner.preClose(Map<PdfName, Integer>) contains this code:

if (certificationLevel > 0) {
    // add DocMDP entry to root
    PdfDictionary docmdp = new PdfDictionary();
    docmdp.put(PdfName.DocMDP, cryptoDictionary.getPdfObject());
    document.getCatalog().put(PdfName.Perms, docmdp); // TODO: setModified?
}

document.close();

The cryptoDictionary.getPdfObject()) is the signature dictionary I mentioned above. During document.close() it is added to an object stream unless it has been written to the output before. Thus, you simply have to add a call to flush that object right before that close call and by parameter make clear that it shall not be added to an object stream:

cryptoDictionary.getPdfObject().flush(false);

With that patch in place, the PDFs your code returns are not damaged as above anymore.


As an aside, iText 5 does contain a similar line in the corresponding PdfSignatureAppearance.preClose(HashMap<PdfName, Integer>) right above the if block corresponding to the if block above. It seems to have been lost during refactoring to iText 7.