Hot questions for Using PDFBox in sign

Question:

I'm trying to produce PDF with visual signature and pdfbox. I have two streams and it seems that pdfbox can deal only with files. I didn't manage to make it work without three temporary files. I can see from here that API has changed, but still it deals with files.

public void signPdf(InputStream originalPdf, OutputStream signedPdf,
        InputStream image, float x, float y,
        String name, String location, String reason) {

    File temp = null;
    File temp2 = null;
    File scratchFile = null;
    RandomAccessFile randomAccessFile = null;
    OutputStream tempOut = null;
    InputStream tempIn = null;
    try {
        /* Copy original to temporary file */
        temp = File.createTempFile("signed1", ".tmp");
        tempOut = new FileOutputStream(temp);
        copyStream(originalPdf, tempOut);
        tempOut.close();

        /* Read temporary file to second temporary file and stream */
        tempIn = new FileInputStream(temp);
        temp2 = File.createTempFile("signed2", ".tmp");
        tempOut = new FileOutputStream(temp2);
        copyStream(tempIn, tempOut);
        tempIn.close();
        tempIn = new FileInputStream(temp2);

        scratchFile = File.createTempFile("signed3", ".bin");
        randomAccessFile = new RandomAccessFile(scratchFile, "rw");

        /* Read temporary file */
        PDDocument document = PDDocument.load(temp, randomAccessFile);
        document.getCurrentAccessPermission().setCanModify(false);

        PDSignature signature = new PDSignature();
        signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
        signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
        signature.setName(name);
        signature.setLocation(location);
        signature.setReason(reason);
        signature.setSignDate(Calendar.getInstance());

        PDVisibleSignDesigner signatureDesigner = new PDVisibleSignDesigner(
                document, image, document.getNumberOfPages());
        signatureDesigner.xAxis(250).yAxis(60).zoom(-90).signatureFieldName("signature");

        PDVisibleSigProperties signatureProperties = new PDVisibleSigProperties();
        signatureProperties.signerName(name).signerLocation(location)
                .signatureReason(reason).preferredSize(0).page(1)
                .visualSignEnabled(true).setPdVisibleSignature(signatureDesigner)
                .buildSignature();

        SignatureOptions options = new SignatureOptions();
        options.setVisualSignature(signatureProperties);

        document.addSignature(signature, dataSigner, options);

        /* Sign */
        document.saveIncremental(tempIn, tempOut);
        document.close();
        tempIn.close();

        /* Copy temporary file to an output stream */
        tempIn = new FileInputStream(temp2);
        copyStream(tempIn, signedPdf);
    } catch (IOException e) {
        logger.error("PDF signing failure", e);
    } catch (COSVisitorException e) {
        logger.error("PDF creation failure", e);
    } catch (SignatureException e) {
        logger.error("PDF signing failure", e);
    } finally {
        closeStream(originalPdf);
        closeStream(signedPdf);
        closeStream(randomAccessFile);
        closeStream(tempOut);
        deleteTempFile(temp);
        deleteTempFile(temp2);
        deleteTempFile(scratchFile);
    }
}

private void deleteTempFile(File tempFile) {
    if (tempFile != null && tempFile.exists() && !tempFile.delete()) {
        tempFile.deleteOnExit();
    }
}

private void closeStream(Closeable is) {
    if (is!= null) {
        try {
            is.close();
        } catch (IOException e) {
            logger.error("failure", e);
        }
    }
}

private void copyStream(InputStream is, OutputStream os) throws IOException {
    byte[] buffer = new byte[1024];
    int c;
    while ((c = is.read(buffer)) != -1) {
        os.write(buffer, 0, c);
    }
    is.close();
}

Apart from file madness I don't see any text on the signature. This is how result looks like:

and this is how it looks, when I do similar thing with itext library

Why name, location and reason missing from the visual signature representation? How can I fix that?


Answer:

Why name, location and reason missing from the visual signature representation?

They are not there because they are not drawn.

The default way of iText to represent a visualized signature is by adding those information to the visualization.

The default way of PDFBox' PDVisibleSigBuilder to represent a visualized signature is without such information.

Neither is wrong or right, both merely are defaults.

The canonical place where people shall look for such information is the signature panel after all.

How can I fix that?

The actual contents of the signature visualization are created by a PDVisibleSigBuilder instance during signatureProperties.buildSignature():

public void buildSignature() throws IOException
{
    PDFTemplateBuilder builder = new PDVisibleSigBuilder();
    PDFTemplateCreator creator = new PDFTemplateCreator(builder);
    setVisibleSignature(creator.buildPDF(getPdVisibleSignature()));
}

Thus, by replacing

    signatureProperties.signerName(name).signerLocation(location)
            .signatureReason(reason).preferredSize(0).page(1)
            .visualSignEnabled(true).setPdVisibleSignature(signatureDesigner)
            .buildSignature();

in your code by

    signatureProperties.signerName(name).signerLocation(location)
            .signatureReason(reason).preferredSize(0).page(1)
            .visualSignEnabled(true).setPdVisibleSignature(signatureDesigner);

    PDFTemplateBuilder builder = new ExtSigBuilder();
    PDFTemplateCreator creator = new PDFTemplateCreator(builder);
    signatureProperties.setVisibleSignature(creator.buildPDF(signatureProperties.getPdVisibleSignature()));

for a customized version ExtSigBuilder of this PDVisibleSigBuilder class, you can draw anything you want there, e.g.:

class ExtSigBuilder extends PDVisibleSigBuilder
{
    String fontName;

    public void createImageForm(PDResources imageFormResources, PDResources innerFormResource,
            PDStream imageFormStream, PDRectangle formrect, AffineTransform affineTransform, PDJpeg img)
            throws IOException
    {
        super.createImageForm(imageFormResources, innerFormResource, imageFormStream, formrect, affineTransform, img);

        PDFont font = PDType1Font.HELVETICA;
        fontName = getStructure().getImageForm().getResources().addFont(font);

        logger.info("Added font to image form: " + fontName);
    }

    public void injectAppearanceStreams(PDStream holderFormStream, PDStream innterFormStream, PDStream imageFormStream,
            String imageObjectName, String imageName, String innerFormName, PDVisibleSignDesigner properties)
            throws IOException
    {
        super.injectAppearanceStreams(holderFormStream, innterFormStream, imageFormStream, imageObjectName, imageName, innerFormName, properties);

        String imgFormComment = "q " + 100 + " 0 0 50 0 0 cm /" + imageName + " Do Q\n";
        String text = "BT /" + fontName + " 10 Tf (Hello) Tj ET\n";
        appendRawCommands(getStructure().getImageFormStream().createOutputStream(), imgFormComment + text);

        logger.info("Added text commands to image form: " + text);
    }
}

writes "Hello" in Helvetica at size 10 atop the lower left of the image form (the form actually displaying something).

PS: In my opinion the object oriented structure behind this should be completely overhauled.

Question:

I'm trying to sign a PDF using PDFBox, and it does sign but when I open the document in adobe reader I get the following message "Document has been altered or corrupted since it was signed" can someone please help me find the problem.

The keystore was created with "keytool -genkeypair -storepass 123456 -storetype pkcs12 -alias test -validity 365 -v -keyalg RSA -keystore keystore.p12"

Using pdfbox-1.8.9 and bcpkix-jdk15on-1.52

Here is my code:

import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.exceptions.SignatureException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;

import java.io.*;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Calendar;
import java.util.Collections;
import java.util.Enumeration;

public class CreateSignature implements SignatureInterface {
    private static PrivateKey privateKey;
    private static Certificate certificate;

    boolean signPdf(File pdfFile, File signedPdfFile) {

        try (
                FileInputStream fis1 = new FileInputStream(pdfFile);
                FileInputStream fis = new FileInputStream(pdfFile);
                FileOutputStream fos = new FileOutputStream(signedPdfFile);
                PDDocument doc = PDDocument.load(pdfFile)) {
            int readCount;
            byte[] buffer = new byte[8 * 1024];
            while ((readCount = fis1.read(buffer)) != -1) {
                fos.write(buffer, 0, readCount);
            }

            PDSignature signature = new PDSignature();
            signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
            signature.setName("NAME");
            signature.setLocation("LOCATION");
            signature.setReason("REASON");
            signature.setSignDate(Calendar.getInstance());
            doc.addSignature(signature, this);
            doc.saveIncremental(fis, fos);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public byte[] sign(InputStream is) throws SignatureException, IOException {
        try {
            BouncyCastleProvider BC = new BouncyCastleProvider();
            Store certStore = new JcaCertStore(Collections.singletonList(certificate));

            CMSTypedDataInputStream input = new CMSTypedDataInputStream(is);
            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
            ContentSigner sha512Signer = new JcaContentSignerBuilder("SHA256WithRSA").setProvider(BC).build(privateKey);

            gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
                    new JcaDigestCalculatorProviderBuilder().setProvider(BC).build()).build(sha512Signer, new X509CertificateHolder(certificate.getEncoded())
            ));
            gen.addCertificates(certStore);
            CMSSignedData signedData = gen.generate(input, false);

            return signedData.getEncoded();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) throws IOException, GeneralSecurityException, SignatureException, COSVisitorException {
        char[] password = "123456".toCharArray();

        KeyStore keystore = KeyStore.getInstance("PKCS12");
        keystore.load(new FileInputStream("/home/user/Desktop/keystore.p12"), password);

        Enumeration<String> aliases = keystore.aliases();
        String alias;
        if (aliases.hasMoreElements()) {
            alias = aliases.nextElement();
        } else {
            throw new KeyStoreException("Keystore is empty");
        }
        privateKey = (PrivateKey) keystore.getKey(alias, password);
        Certificate[] certificateChain = keystore.getCertificateChain(alias);
        certificate = certificateChain[0];

        File inFile = new File("/home/user/Desktop/sign.pdf");
        File outFile = new File("/home/user/Desktop/sign_signed.pdf");
        new CreateSignature().signPdf(inFile, outFile);
    }
}


class CMSTypedDataInputStream implements CMSTypedData {
    InputStream in;

    public CMSTypedDataInputStream(InputStream is) {
        in = is;
    }

    @Override
    public ASN1ObjectIdentifier getContentType() {
        return PKCSObjectIdentifiers.data;
    }

    @Override
    public Object getContent() {
        return in;
    }

    @Override
    public void write(OutputStream out) throws IOException,
            CMSException {
        byte[] buffer = new byte[8 * 1024];
        int read;
        while ((read = in.read(buffer)) != -1) {
            out.write(buffer, 0, read);
        }
        in.close();
    }
}

Answer:

Fixing "Document has been altered or corrupted"

The mistake is that you call PDDocument.saveIncremental with an InputStream merely covering the original PDF:

FileInputStream fis1 = new FileInputStream(pdfFile);
FileInputStream fis = new FileInputStream(pdfFile);
FileOutputStream fos = new FileOutputStream(signedPdfFile);
...
doc.saveIncremental(fis, fos);

But the method expects the InputStream to cover the original file plus the changes made to prepare for signing.

Thus, fis also needs to point to signedPdfFile, and as that file might not exist before, the order of creating fis and fos must be switched>

FileInputStream fis1 = new FileInputStream(pdfFile);
FileOutputStream fos = new FileOutputStream(signedPdfFile);
FileInputStream fis = new FileInputStream(signedPdfFile);
...
doc.saveIncremental(fis, fos);

Unfortunately the JavaDocs don't Point this out.

Another issue

There is another issue with the generated signature. If you look at the ASN.1 dump of a sample result, you'll see something starting like this:

    <30 80>
   0 NDEF: SEQUENCE {
    <06 09>
   2    9:   OBJECT IDENTIFIER signedData (1 2 840 113549 1 7 2)
         :     (PKCS #7)
    <A0 80>
  13 NDEF:   [0] {
    <30 80>
  15 NDEF:     SEQUENCE {
    <02 01>
  17    1:       INTEGER 1
    <31 0F>
  20   15:       SET {

The NDEF length indications show that the indefinite-length method is used for encoding these outer layers of the signature container. Use of this method is allowed in the Basic Encoding Rules (BER) but not in the more strict Distinguished Encoding Rules (DER). While using BER for the outer layers is allowed for generic PKCS#7/CMS signatures, the PDF specification clearly requires:

When PKCS#7 signatures are used, the value of Contents shall be a DER-encoded PKCS#7 binary data object containing the signature.

(section 12.8.3.3.1 "PKCS#7 Signatures as used in ISO 32000" / "General" in ISO 32000-1)

Thus, strictly speaking your signature is even structurally invalid. Usually, though, this is not detected by PDF signature verification services because most of them use standard PKCS#7/CMS libraries or methods for verifying the signature containers.

If you want to make sure that your signatures are truly valid PDF signatures, you can achieve this by replacing

return signedData.getEncoded();

by something like

ByteArrayOutputStream baos = new ByteArrayOutputStream();
DEROutputStream dos = new DEROutputStream(baos);
dos.writeObject(signedData.toASN1Structure());
return baos.toByteArray();

Now the whole signature object is DER-encoded.

(You can find a test creating signatures both with your original and the fixed code either with or without improved encoding here: SignLikeLoneWolf.java)

Question:

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

The code nicely adds the image and the 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 that question describes the problem in more detail: the problem is that my code leaves a mix of cross reference types in the document (streams and tables). Indeed, some documents won't even open because of the problems that this creates.

My question is: how do I prevent this? How do I add an image to an existing pdf document without creating multiple cross reference types?

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

Answer:

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.

Question:

I want to sign a InputStream from a PDF file without using a temporary file. Here I convert InputStream to File and this work fine :

InputStream inputStream = this.signatureObjPAdES.getSignatureDocument().getInputStream();
OutputStream outputStream = new FileOutputStream(new File("C:/temp.pdf"));
int read = 0;
byte[] bytes = new byte[1024];

while ((read = inputStream.read(bytes)) != -1) {
    outputStream.write(bytes, 0, read);
}

PDDocument document = PDDocument.load(new File("C:/temp.pdf"));

...

document.addSignature(new PDSignature(this.dts.getDocumentTimeStamp()), this);
document.saveIncremental(new FileOutputStream("C:/result.pdf");
document.close();

But I want to do this directly :

PDDocument document = PDDocument.load(inputStream);

Problem: at run

Exception in thread "main" java.lang.NullPointerException
at java.io.RandomAccessFile.<init>(Unknown Source)
at org.apache.pdfbox.io.RandomAccessBufferedFileInputStream.<init>(RandomAccessBufferedFileInputStream.java:77)
at org.apache.pdfbox.pdmodel.PDDocument.saveIncremental(PDDocument.java:961)

All ideas are welcome. Thank you.

EDIT: It's now working with the release of PDFBox 2.0.0.


Answer:

The cause

The immediate hindrance is in the method PDDocument.saveIncremental() itself:

public void saveIncremental(OutputStream output) throws IOException
{
    InputStream input = new RandomAccessBufferedFileInputStream(incrementalFile);
    COSWriter writer = null;
    try
    {
        writer = new COSWriter(output, input);
        writer.write(this, signInterface);
        writer.close();
    }
    finally
    {
        if (writer != null)
        {
            writer.close();
        }
    }
}

(PDDocument.java)

The member incrementalFile used in the first line is only set during a PDDocument.load with a File parameter.

Thus, this method cannot be used.

A work-around

Fortunately the method PDDocument.saveIncremental() only uses methods and values publicly available with the sole exception of signInterface, but you know the value of it because you set it in your code in the line right before the saveIncremental call:

document.addSignature(new PDSignature(this.dts.getDocumentTimeStamp()), this);
document.saveIncremental(new FileOutputStream("C:/result.pdf"));

Thus, instead of calling PDDocument.saveIncremental() you can do the equivalent in your code.

To do so you furthermore need a replacement value for the InputStream input. It needs to return a stream with the identical content as inputStream in your

PDDocument document = PDDocument.load(inputStream);

So you need to use that stream twice. As you have not said whether that inputStream can be reset, we'll first copy it into a byte[] which we forward both to PDDocument.load and new COSWriter.

Thus, replace your

PDDocument document = PDDocument.load(inputStream);

...

document.addSignature(new PDSignature(this.dts.getDocumentTimeStamp()), this);
document.saveIncremental(new FileOutputStream("C:/result.pdf"));
document.close();

by

byte[] inputBytes = IOUtils.toByteArray(inputStream);
PDDocument document = PDDocument.load(new ByteArrayInputStream(inputBytes));

...

document.addSignature(new PDSignature(this.dts.getDocumentTimeStamp()), this);
saveIncremental(new FileOutputStream("C:/result.pdf"),
    new ByteArrayInputStream(inputBytes), document, this);
document.close();

and add a new method saveIncremental to your class inspired by the original PDDocument.saveIncremental():

void saveIncremental(OutputStream output, InputStream input, PDDocument document, SignatureInterface signatureInterface) throws IOException
{
    COSWriter writer = null;
    try
    {
        writer = new COSWriter(output, input);
        writer.write(document, signatureInterface);
        writer.close();
    }
    finally
    {
        if (writer != null)
        {
            writer.close();
        }
    }
}
On the side

I said above

As you have not said whether that inputStream can be reset, we'll first copy it into a byte[] which we forward both to PDDocument.load and new COSWriter.

Actually there is another reason to do so: COSWriter.doWriteSignature() retrieves the length of the original PDF like this:

long inLength = incrementalInput.available();

(COSWriter.java)

The documentation of InputStream.available() states, though:

Note that while some implementations of InputStream will return the total number of bytes in the stream, many will not.

To re-use inputStream instead of using a byte[] and ByteArrayInputStreams as above, therefore, inputStream not only needs to support reset() but also needs to be one of the few InputStream implementations which return the total number of bytes in the stream as available.

FileInputStream and ByteArrayInputStream both do return the total number of bytes in the stream as available.

There may still be more issues when using generic InputStreams instead of these two.

Question:

Currently i have a client-server application that, given a PDF file, signs it (with the server certificate), attachs the signature with the original file and returns the output back to the client (all of this is achieved with PDFBox). I have a Signature handler, which is my External Signing Support (where content is the PDF file)

    public byte[] sign(InputStream content) throws IOException {
    try {
        System.out.println("Generating CMS signed data");
        CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
        ContentSigner sha1Signer = new JcaContentSignerBuilder("Sha1WithRSA").build(privateKey);
        generator.addSignerInfoGenerator(
                new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build())
                        .build(sha1Signer, new X509CertificateHolder(certificate.getEncoded())));
        CMSTypedData cmsData = new CMSProcessableByteArray(IOUtils.toByteArray(content));
        CMSSignedData signedData = generator.generate(cmsData, false);

        return signedData.getEncoded();
    } catch (GeneralSecurityException e) {
        throw new IOException(e);
    } catch (CMSException e) {
        throw new IOException(e);
    } catch (OperatorCreationException e) {
        throw new IOException(e);
    }
}

It works fine, but i was thinking - what if the PDF file is too big to be uploaded? ex: 100mb... it would take forever! Given that, i am trying to figure out, if instead of signing the PDF file, is it possible to just sign the Hash (ex SHA1) of that file and than the client puts it all together in the end?

Update:

I have been trying to figure this out, and now my signing method is:

    @Override
public byte[] sign(InputStream content) throws IOException {
    // testSHA1WithRSAAndAttributeTable
    try {
        MessageDigest md = MessageDigest.getInstance("SHA1", "BC");
        List<Certificate> certList = new ArrayList<Certificate>();
        CMSTypedData msg = new CMSProcessableByteArray(IOUtils.toByteArray(content));

        certList.add(certificate);

        Store certs = new JcaCertStore(certList);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();

        Attribute attr = new Attribute(CMSAttributes.messageDigest,
                new DERSet(new DEROctetString(md.digest(IOUtils.toByteArray(content)))));

        ASN1EncodableVector v = new ASN1EncodableVector();

        v.add(attr);

        SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
                .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v)));

        AlgorithmIdentifier sha1withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1withRSA");

        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
        InputStream in = new ByteArrayInputStream(certificate.getEncoded());
        X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in);

        gen.addSignerInfoGenerator(builder.build(
                new BcRSAContentSignerBuilder(sha1withRSA,
                        new DefaultDigestAlgorithmIdentifierFinder().find(sha1withRSA))
                                .build(PrivateKeyFactory.createKey(privateKey.getEncoded())),
                new JcaX509CertificateHolder(cert)));

        gen.addCertificates(certs);

        CMSSignedData s = gen.generate(new CMSAbsentContent(), false);
        return new CMSSignedData(msg, s.getEncoded()).getEncoded();

    } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        throw new IOException(e);
    }

}

And i am merging the signature with the PDF with pdfbox

            ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output);
        byte[] cmsSignature = sign(externalSigning.getContent());
        externalSigning.setSignature(cmsSignature);

The problem is that Adobe says the signature is invalid because the "document has been altered or corrupted since it was signed". Can anyone help?


Answer:

In his update the OP nearly has it right, there merely are two errors:

  • He tries to read the InputStream parameter content twice:

    CMSTypedData msg = new CMSProcessableByteArray(IOUtils.toByteArray(content));
    [...]
    Attribute attr = new Attribute(CMSAttributes.messageDigest,
            new DERSet(new DEROctetString(md.digest(IOUtils.toByteArray(content)))));
    

    Thus, all data had already been read from the stream before the second attempt which consequently returned an empty byte[]. So the message digest attribute contained a wrong hash value.

  • He creates the final CMS container in a convoluted way:

    return new CMSSignedData(msg, s.getEncoded()).getEncoded();
    

Reducing the latter to what is actually needed, it turns out that there is no need for the CMSTypedData msg anymore. Thus, the former is implicitly resolved.

After re-arranging the digest calculation to the top of the method and additionally switching to SHA256 (as SHA1 is deprecated in many contexts, I prefer to use a different hash algorithm) and allowing for a certificate chain instead of a single certificate, the method looks like this:

// Digest generation step
MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
byte[] digest = md.digest(IOUtils.toByteArray(content));

// Separate signature container creation step
List<Certificate> certList = Arrays.asList(chain);
JcaCertStore certs = new JcaCertStore(certList);

CMSSignedDataGenerator gen = new CMSSignedDataGenerator();

Attribute attr = new Attribute(CMSAttributes.messageDigest,
        new DERSet(new DEROctetString(digest)));

ASN1EncodableVector v = new ASN1EncodableVector();

v.add(attr);

SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
        .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v)));

AlgorithmIdentifier sha256withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA");

CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
InputStream in = new ByteArrayInputStream(chain[0].getEncoded());
X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in);

gen.addSignerInfoGenerator(builder.build(
        new BcRSAContentSignerBuilder(sha256withRSA,
                new DefaultDigestAlgorithmIdentifierFinder().find(sha256withRSA))
                        .build(PrivateKeyFactory.createKey(pk.getEncoded())),
        new JcaX509CertificateHolder(cert)));

gen.addCertificates(certs);

CMSSignedData s = gen.generate(new CMSAbsentContent(), false);
return s.getEncoded();

(CreateSignature method signWithSeparatedHashing)

Used in a fairly minimal signing code frame

void sign(PDDocument document, OutputStream output, SignatureInterface signatureInterface) throws IOException
{
    PDSignature signature = new PDSignature();
    signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
    signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
    signature.setName("Example User");
    signature.setLocation("Los Angeles, CA");
    signature.setReason("Testing");
    signature.setSignDate(Calendar.getInstance());
    document.addSignature(signature);
    ExternalSigningSupport externalSigning =
            document.saveIncrementalForExternalSigning(output);
    byte[] cmsSignature = signatureInterface.sign(externalSigning.getContent());
    externalSigning.setSignature(cmsSignature);
}

(CreateSignature method sign)

like this

try (   InputStream resource = getClass().getResourceAsStream("test.pdf");
        OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "testSignedWithSeparatedHashing.pdf"));
        PDDocument pdDocument = PDDocument.load(resource)   )
{
    sign(pdDocument, result, data -> signWithSeparatedHashing(data));
}

(CreateSignature test method testSignWithSeparatedHashing)

results in properly signed PDFs, as proper at least as the certificates and private key in question are for the task at hand.


One remark:

The OP used IOUtils.toByteArray(content)) (and so do I in the code above). But considering the OP's starting remark

what if the PDF file is too big to be uploaded? ex: 100mb

doing so is not such a great idea as it loads a big file into memory at once only for hashing. If one really wants to consider the resource footprint of one's application, one should read the stream a few KB at a time and consecutively digest the data using MessageDigest.update and only use MessageDigest.digest at the end to get the result hash value.

Question:

We 're trying to sign a PDF document using the CAdES method and the examples in dss-cookbook as a starting point using the latest version (4.6.RC1).

Following the example from SignPdfPadesBDetached.java, we have succesfully signed a PDF document using PAdES. However, since there is no example for CAdES, we tried adapting the above example to use CAdES, but it doesn't work. Specifically the generated PDF document has a size of only 7k instead of the expected 2.5MB and the following error is displayed when trying to open the PDF: We assume the 7k is actually only the signature so that the actual document is not included. The settings we use are:

  • SignatureLevel.CAdES_BASELINE_B
  • SignaturePackaging.DETACHED
  • DigestAlgorithm.SHA256

And the relative's method code is currently this:

public static void signPdfWithCades(DSSDocument toSignDocument) {

    LOG.info("Signing PDF with CADES B");

    try {
        AbstractSignatureTokenConnection signingToken = new Pkcs12SignatureToken("password", KEYSTORE_PATH);
        DSSPrivateKeyEntry privateKey = signingToken.getKeys().get(0);

        // Preparing parameters for the CAdES signature
        CAdESSignatureParameters parameters = new CAdESSignatureParameters();
        // We choose the level of the signature (-B, -T, -LT, -LTA).
        parameters.setSignatureLevel(SignatureLevel.CAdES_BASELINE_B);
        // We choose the type of the signature packaging (ENVELOPING, DETACHED).
        parameters.setSignaturePackaging(SignaturePackaging.DETACHED);
        // We set the digest algorithm to use with the signature algorithm. You must use the
        // same parameter when you invoke the method sign on the token. The default value is
        // SHA256
        parameters.setDigestAlgorithm(DigestAlgorithm.SHA256);

        // We set the signing certificate
        parameters.setSigningCertificate(privateKey.getCertificate());
        // We set the certificate chain
        parameters.setCertificateChain(privateKey.getCertificateChain());

        // Create common certificate verifier
        CommonCertificateVerifier commonCertificateVerifier = new CommonCertificateVerifier();
        // Create PAdES xadesService for signature
        CAdESService service = new CAdESService(commonCertificateVerifier);

        // Get the SignedInfo segment that need to be signed.
        ToBeSigned dataToSign = service.getDataToSign(toSignDocument, parameters);

        // This function obtains the signature value for signed information using the
        // private key and specified algorithm
        DigestAlgorithm digestAlgorithm = parameters.getDigestAlgorithm();
        SignatureValue signatureValue = signingToken.sign(dataToSign, digestAlgorithm, privateKey);

        // We invoke the cadesService to sign the document with the signature value obtained in
        // the previous step.
        DSSDocument signedDocument = service.signDocument(toSignDocument, parameters, signatureValue);

        LOG.info("Signed PDF size = " + signedDocument.getBytes().length);

        //We use the DSSUtils to Save to file
        DSSUtils.saveToFile(signedDocument.openStream(), "target/signedPdfCadesBDetached.pdf");
    } catch (Exception e) {
        LOG.error(e.getMessage(), e);
    }
}

The corresponding method for signing with PAdES is similar to the above, adjusted to PAdES (that is, we there used PAdESSignatureParameters, SignatureLevel.PAdES_BASELINE_B and PAdESService) classes.

Please note that the SD-DSS project is not hosted in the Maven Central repository, so we had to make an explicit reference to it:

<repositories>
    <repository>
        <id>europa</id>
        <url>https://joinup.ec.europa.eu/nexus/content/groups/public/</url>
    </repository>
</repositories>

In addition, I believe we included all of the required/corresponding dependencies in our pom.xml:

<dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-token</artifactId>
    <version>4.6.RC1</version>
</dependency>
<dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-pades</artifactId>
    <version>4.6.RC1</version>
</dependency>
<dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-cades</artifactId>
    <version>4.6.RC1</version>
</dependency>
<dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-document</artifactId>
    <version>4.6.RC1</version>
</dependency>

Prior to this, we also gave a try to PDFBox, but the documentation wasn't so helpful, according to what we want to accomplish.

Any idea what is wrong here? Changing the packaging ENVELOPING makes no difference either. Is the method for signing with CAdES so different that the PAdES example should not be used as a guide?


Answer:

In general,

PAdES signatures are specifically profiled signatures embedded into a PDF. Thus, your PAdES signed PDF can be opened in Adobe Reader, and Adobe Reader can recognize the signature, verify it, and display the result of that in its signature panel.

CAdES signatures are specifically profiled signatures which either are in a separate file or which contain the signed data. Neither of these formats is recognized by Adobe Reader, in case of the separate file you can open the original PDF but the Reader does not see the signature, in case of the contained PDF the Reader either cannot open the file at all or (as the Reader ignores some leading and trailing extra bytes) considers the signature ignorable trash.

You only need a PDF aware library (like PDFBox) for PAdES signatures, for CAdES signatures the PDF is treated like generic data bytes.


In your case, therefore, i.e. for

  • SignatureLevel.CAdES_BASELINE
  • SignaturePackaging.DETACHED

your 7K indeed is the mere signature in a separate file and you have to keep or forward both the PDF and the signature, the PDF for viewing and the signature for verification.

Thus,

Specifically the generated PDF document has a size of only 7k instead of the expected 2.5MB ...

We assume the 7k is actually only the signature so that the actual document is not included.

Your assumption is correct, and also the behavior is correct. Yout expectations are the issue.


Some confusion might result from the facts that the signature container embedded into a PDF in case of a PAdES signature, when extracted, proves to be in CAdES format, that the matching PDF subfilter is called ETSI.CAdES.detached, and that (at least in the most recent draft at hand) the PDF 2.0 specification will treat the PAdES signatures in a section titled "12.8.3.4 CAdES signatures as used in PDF (PDF 2.0)".

Nonetheless, if you are talking about PAdES signatures, you are talking about ETSI AdES signatures integrated in PDFs. If you are talking about CAdES signatures, you are talking about ETSI AdES CMS signatures independant of a specific document format which may be detached from the signed data or which may wrap them.


According to your comments, in particular this one

Signing the PDF using ETSI.CAdES.DETACHED filter is the exact requirement

you in the end do not want to create a CAdES signature but instead a PAdES signature, more exactly you want to do so according to the Part 3: PAdES Enhanced - PAdES-BES and PAdES-EPES Profiles and not according to Part 2: PAdES Basic - Profile based on ISO 32000-1 which uses the subfilters adbe.pkcs7.detached and adbe.pkcs7.sha1.

(To get the requirement straight, that is the subfilter value, not the filter value.)

This is exactly what the dss cookbook examples SignPdfPadesB, SignPdfPadesBDetached, and SignPdfPadesBVisible should be all about:

  • they all in their JavaDoc class comment claim to show How to sign PDF Document with PAdES-BASELINE-B,
  • the cookbook asciidoc/dss-documentation.adoc for PAdES Baseline profiles refers to ETSI TS 103 172,
  • and that standard specifies:

    6 Requirements for B-Level Conformance

    This clause defines requirements that PAdES signatures claiming conformance to the B-Level have to fulfil.

    The current clause specifies compliance requirements for short-term electronic signatures. This clause actually profiles PAdES-BES (signatures that do not incorporate signature-policy-identifier) and PAdES-EPES (signatures that do incorporate signature-policy-identifier) signatures.

    (ETSI TS 103 172 V2.1.1 (2012-03))

Unfortunately I cannot now verify that the samples do what they claim as my eclipse dss projects are all red with problems.

If they do, though, it looks like you in the beginning already had what you wanted:

Following the example from SignPdfPadesBDetached.java, we have succesfully signed a PDF document using PAdES.

You may share a sample PDF signed with that example for analysis to be sure.

Question:

I am trying to figure out whether PDFBox supports signing of existing (emtpy) signature form fields. I checked the examples provided however all only seem to add new fields. There was another post where the OP states:

"Pre-existing signature fields are not affected by pdfbox as pdfbox appears not to be able to reference them."

Then however this has been written a year ago and there seems to be some effort on the signature functionality. So can anyone tell me the if it is possible (if so how) to reference existings signature fields? Or maybe it is planned?

Update I implemented as you suggested the following functionality:

PDDocumentCatalog docCatalog = doc.getDocumentCatalog();
PDAcroForm acroForm = docCatalog.getAcroForm();
PDField field = acroForm.getField("exampleSignature");
PDSignature signature = ((PDSignatureField)field).getSignature();

However signature is alway null. After checking the PDF spec it perfectly makes sense, since empty signature fields never have the signature dictionary set. When adding a signature dictionary e.g. the values for the filter, Contents, ByteRange etc. must be filled but can only be filled with meaningfull values to the time of signing...


Answer:

Starting with 2.0.4, but already in the snapshot builds, it is possible to sign existing (empty) signature form fields. (It will not work with 2.0.3, even if you use the updated code example from upcoming 2.0.4, because the library code had several bugs that have been fixed). The example code can be found here. Two things are new in the example code:

  • visibleSignatureProperties.buildSignature(); has been moved
  • a call signature = findExistingSignature(doc, "Signature1"); has been added.

What this does is to search for the signature field named "Signature1", and if found, it creates a signature dictionary (the /V component). Because this signature object is passed to the doc.addSignature() call, PDFBox will be able to detect that the parent field already exists and won't create a new one.

More details can be found in PDFBOX-3525.

Question:

I am using pdfbox-1.8.8 to do the signing function on PDF file.

It works well with PDF file in portrait mode. But with landscape file, I have an issue

It looks like the coordinate is wrong for the landscape file.

Does anyone know what is wrong with the file ?

Here is the link of pdf file

Here is the code I used to sign

public void signDetached(String inputFilePath, String outputFilePath, String signatureImagePath, Sign signProperties) {
    OutputStream outputStream = null;
    InputStream inputStream = null;
    PDDocument document = null;
    InputStream signImageStream = null;

    try {
        setTsaClient(null);
        document = PDDocument.load(inputFilePath);
        // create signature dictionary
        PDSignature signature = new PDSignature();
        signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
        signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
        signature.setName("VANDUC1102");
        signature.setLocation(null);
        String displayName = "Hello World, Document signed by VANDUC1102";
        String reason = reasonText+ " " + displayName;
        signature.setReason(reason);

        // the signing date, needed for valid signature
        signature.setSignDate(Calendar.getInstance());            
        int signatureInPage = signProperties.getPageNumber() + 1;
        signImageStream = new FileInputStream(new File(signatureImagePath));
        PDVisibleSignDesigner visibleSig = new PDVisibleSignDesigner(inputFilePath, signImageStream, signatureInPage);

        float xAxis = convertPixel2Point(signProperties.getX()) ;
        float yAxis = convertPixel2Point(signProperties.getY());               
        float signImageHeight = convertPixel2Point(signImageHeight);    
        float signImageWidth = convertPixel2Point(signImageWidth);

        visibleSig.xAxis(xAxis)
                .yAxis(yAxis)
                .zoom(0)
                .signatureFieldName("Signature")
                .height(signImageHeight)
                .width(signImageWidth);
        PDVisibleSigProperties signatureProperties = new PDVisibleSigProperties();

        signatureProperties.signerName(eiUser.getName())
                 .signerLocation(null)
                 .signatureReason(reason)
                 .preferredSize(0)
                 .page(signProperties.getPageNumber())
                 .visualSignEnabled(true)
                 .setPdVisibleSignature(visibleSig)
                 .buildSignature();
         // register signature dictionary and sign interface
        SignatureOptions signatureOptions = new SignatureOptions();
        signatureOptions.setVisualSignature(signatureProperties);
        signatureOptions.setPage(signatureInPage);
        document.addSignature(signature, this, signatureOptions);

        File outputFile = new File(outputFilePath);
        outputStream = new FileOutputStream(outputFile);
        inputStream = new FileInputStream(inputFilePath);
        IOUtils.copyStream(inputStream, outputStream);
        document.saveIncremental(inputStream, outputStream);
        outputStream.flush();
    } catch (COSVisitorException | SignatureException | IOException ex) {
        log.error("signDetached ", ex);
    } finally {
        IOUtils.closeStream(outputStream);
        IOUtils.closeStream(inputStream);
        IOUtils.closeStream(signImageStream);
        IOUtils.closeStream(document);
    }
}
private float convertPixel2Point(float pixel){
    return pixel * (float) 72/96;
}

As I said this code work well with portrait PDF

Thanks.


Answer:

The page in question has a non-zero Rotate value. The PDFBox visual signing classes completely ignore this value, so one has to give it the coordinates and dimensions as if the page was not rotated.

This can be done by adding the following switch statement:

float xAxis = convertPixel2Point(/*signProperties.getX()*/x) ;
float yAxis = convertPixel2Point(/*signProperties.getY()*/y);               
float signImageHeight = convertPixel2Point(/*signImageHeight*/324);    
float signImageWidth = convertPixel2Point(/*signImageWidth*/309);

int rotation = getPageRotation(inputFilePath, page) % 360;
switch (rotation)
{
case 0:
    // all ok;
    break;
case 90:
    visibleSig.affineTransformParams(new byte[] {0, 1, -2, 0, 100, 0})
              .formaterRectangleParams(new byte[]{0, 0, 100, 100});

    float temp = yAxis;
    yAxis = visibleSig.getPageHeight() - xAxis - signImageWidth;
    xAxis = temp;

    temp = signImageHeight;
    signImageHeight = signImageWidth;
    signImageWidth = temp;

    break;
case 180:
    // Implement in a similar fashion
case 270:
    // Implement in a similar fashion
}

visibleSig.xAxis(xAxis)
          .yAxis(yAxis)
          .zoom(0)
          .signatureFieldName("Signature")
          .height(signImageHeight)
          .width(signImageWidth);

and the following method:

private int getPageRotation(String documentPath, int page) throws IOException
{
    try (PDDocument document = PDDocument.load(documentPath))
    {
        List<?> pages = document.getDocumentCatalog().getAllPages();
        PDPage pageObject =(PDPage) pages.get(page);
        return pageObject.getRotation();
    }
}

For Rotate values of 180 and 270, analogous corrections have to be made.

(Test methods testLandscapeOriginal and testLandscapeFixed in SignLikeVanduc1102)

Question:

I am trying to verify digitally signed PDF document in Java.

I'm using Apache PDFBox 2.0.6 to get the signature and the original PDF that was signed, then I'm using Bouncy Castle to verify detached signature(calculate the hash of the original file, verify the signature using signer's public key and compare the results).

I read this article and tried to get the signature bytes and the original PDF bytes using this code:

PDDocument doc = PDDocument.load(signedPDF);
    byte[] origPDF = doc.getSignatureDictionaries().get(0).getSignedContent(signedPDF);
    byte[] signature = doc.getSignatureDictionaries().get(0).getContents(signedPDF);

But, when I save the origPDF to a file I notice that it still has the signature field that the original PDF that was signed didn't have. Also, the size of the save origPDF is 21 kb, while the size of the original PDF was 15 kb. That's probably because of the signature fields.

However, when I try to strip signature fields from the origPDF like this:

public byte[] stripCryptoSig(byte[] signedPDF) throws IOException {

    PDDocument pdDoc = PDDocument.load(signedPDF);
    PDDocumentCatalog catalog = pdDoc.getDocumentCatalog();
    PDAcroForm form = catalog.getAcroForm();
    List<PDField> acroFormFields = form.getFields();
    for (PDField field: acroFormFields) {
        if (field.getFieldType().equalsIgnoreCase("Sig")) {
            System.out.println("START removing Sign Flags");
            field.setReadOnly(true);
            field.setRequired(false);
            field.setNoExport(true);
            System.out.println("END removing Sign Flags");

            /*System.out.println("START flattenning field");            
            field.getAcroForm().flatten();
            field.getAcroForm().refreshAppearances();
            System.out.println("END flattenning field");
            */
            field.getAcroForm().refreshAppearances();
        }
    }

I get the following warrnings:

WARNING: Invalid dictionary, found: '[' but expected: '/' at offset 15756

WARNING: Appearance generation for signature fields not yet implemented - you need to generate/update that manually

And, when I open the PDF in Acrobat the signature field is gone, but I see an image of the signature where the signature used to be as part of the PDF page. This is weird since I thought I removed the signature completely by using byte[] origPDF = doc.getSignatureDictionaries().get(0).getSignedContent(signedPDF);

Btw, I call stripCryptoSig(byte[] signedPDF) function on origPDF, so that's not a mistake.

When I try to verify the signature using bouncy castle I get an exception with the message: message-digest attribute value does not match calculated value

I guess this is because the original PDF that was signed and the PDF I get from PDFBox using doc.getSignatureDictionaries().get(0).getSignedContent(signedPDF); isn't the same.

Here is my bouncy castle verification code:

private SignatureInfo verifySig(byte[] signedData, boolean attached) throws OperatorCreationException, CertificateException, CMSException, IOException {

    SignatureInfo signatureInfo = new SignatureInfo();
    CMSSignedData cmsSignedData;

    if (attached) {
        cmsSignedData = new CMSSignedData(signedData);
    }

    else {
        PDFUtils pdfUtils = new PDFUtils();
        pdfUtils.init(signedData);
        signedData = pdfUtils.getSignature(signedData);
        byte[] sig = pdfUtils.getSignedContent(signedData);
        cmsSignedData = new CMSSignedData(new CMSProcessableByteArray(signedData), sig);
    }

    SignerInformationStore sis = cmsSignedData.getSignerInfos();
    Collection signers = sis.getSigners();
    Store certStore = cmsSignedData.getCertificates();
    Iterator it = signers.iterator();
    signatureInfo.setValid(false);
    while (it.hasNext()) {
        SignerInformation signer = (SignerInformation) it.next();
        Collection certCollection = certStore.getMatches(signer.getSID());

        Iterator certIt = certCollection.iterator();
        X509CertificateHolder cert = (X509CertificateHolder) certIt.next();

        if(signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert))){

            signatureInfo.setValid(true);

            if (attached) {
                CMSProcessableByteArray userData = (CMSProcessableByteArray) cmsSignedData.getSignedContent();
                signatureInfo.setSignedDoc((byte[]) userData.getContent());
            }

            else {
                signatureInfo.setSignedDoc(signedData);
            }


            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

            String signedOnDate = "null";
            String validFromDate = "null";
            String validToDate = "null";

            Date signedOn = this.getSignatureDate(signer);
            Date validFrom = cert.getNotBefore();
            Date validTo = cert.getNotAfter();

            if(signedOn != null) {
                signedOnDate = sdf.format(signedOn);
            }
            if(validFrom != null) {
                validFromDate = sdf.format(validFrom);
            }
            if(validTo != null) {
                validToDate = sdf.format(validTo);
            }

            DefaultAlgorithmNameFinder algNameFinder = new DefaultAlgorithmNameFinder();

            signatureInfo.setSignedBy(IETFUtils.valueToString(cert.getSubject().getRDNs(BCStyle.CN)[0].getFirst().getValue()));
            signatureInfo.setSignedOn(signedOn);
            signatureInfo.setIssuer(IETFUtils.valueToString(cert.getIssuer().getRDNs(BCStyle.CN)[0].getFirst().getValue()));
            signatureInfo.setValidFrom(validFrom);
            signatureInfo.setValidTo(validTo);
            signatureInfo.setVersion(String.valueOf(cert.getVersion()));
            signatureInfo.setSignatureAlg(algNameFinder.getAlgorithmName(signer.getDigestAlgorithmID()) + " WTIH " + algNameFinder.getAlgorithmName(cert.getSubjectPublicKeyInfo().getAlgorithmId()));

            /*signatureInfo.put("Signed by", IETFUtils.valueToString(cert.getSubject().getRDNs(BCStyle.CN)[0].getFirst().getValue()));
            signatureInfo.put("Signed on", signedOnDate);
            signatureInfo.put("Issuer", IETFUtils.valueToString(cert.getIssuer().getRDNs(BCStyle.CN)[0].getFirst().getValue()));
            signatureInfo.put("Valid from", validFromDate);
            signatureInfo.put("Valid to", validToDate);
            signatureInfo.put("Version", "V" + String.valueOf(cert.getVersion()));
            signatureInfo.put("Signature algorithm", algNameFinder.getAlgorithmName(signer.getDigestAlgorithmID()) + " WTIH " + algNameFinder.getAlgorithmName(cert.getSubjectPublicKeyInfo().getAlgorithmId()));*/

            break;
        }
    }

    return signatureInfo;

}

Answer:

You appear to have a misconception concerning the getSignedContent method in particular and PDF signing in general.

I'm using Apache PDFBox 2.0.6 to get the signature and the original PDF that was signed

If by "the original PDF that was signed" you mean a PDF before it entered the signing process, then the second part of your task is impossible for generic signed PDFs.

The reason is that the original PDF before creation of the actual signature is prepared for the act of signing.

This preparation might mean as little as adding a value dictionary (including a gap for later injection of the signature container) for a pre-existing empty signature field as an incremental update leaving the original PDF an untouched starting piece of the resulting signed document.

On the other hand, though, it may additionally mean that a number of the following changes also occur:

  • a new signature field may be created from scratch;
  • an additional page may be added to the document for signature visualizations;
  • extra signature visualizations (either inactive images or actual signature form field widgets) may be added to each page;
  • missing appearances for form fields may be created;
  • the signing application may add its name to meta data entries as document processor, date and time of last change may be updated to the signing time;
  • in case of a pre-existing empty signature field, form fields indicated by that field's field lock dictionary may be set read only;
  • etc pp

If the document was not signed before, these additions need not be added as incremental updates, instead all the objects (changed or unchanged) may be re-ordered, renumbered, indirect object may become direct ones and vice versa, unused objects might be dropped, duplicate objects might be reduced to a single one, fonts of form fields made read-only may be reduced to the actually used glyphs, etc pp

Only for this prepared PDF the actual signature is created and embedded in the gap left in the signature value dictionary.

If you apply your calls

byte[] origPDF = doc.getSignatureDictionaries().get(0).getSignedContent(signedPDF);
byte[] signature = doc.getSignatureDictionaries().get(0).getContents(signedPDF);

to the signed document, origPDF contains the bytes of the signed document except the gap in the signature value dictionary and signature contains the (hex decoded) contents of the gap.

So origPDF in particular contains all the changes done during the preparation; calling it orig, therefore, is vehemently misleading.

Furthermore, as the gap originally reserved for the signature container is missing, it is very likely that these bytes actually don't form a valid PDF anymore: PDFs contain cross references which point to the starting offsets (from the start of the document) of each PDF object; as the gap is missing, the bytes after its former position have moved and offsets going there now are wrong.

Thus, your origPDF merely contains the ensemble of signed bytes which may be very different from the file you consider the original one.


Your verifySig completely ignores the SubFilter of the signature field value dictionary. Depending on that value, the signature bytes you retrieve using getContents might have entirely different contents.

So without your signed PDF, further review of that method does not make sense.

Question:

I'm trying to do the following setup for signing pdfs, broken down into asynchronous steps between a client and a server:

  1. A server receives a pdf and computes it's digest.
  2. Server sends the digest to a client.
  3. Client signs the hash at a later time.
  4. Client sends the signature to server.
  5. Server embeds the signature into the pdf.

I'm basing myself mainly in PDF Signature digest and Create pkcs7 signature from file digest

The second question allowed me to write most of the code, however I'm getting that the integrity of the file has been compromised. I can't seem to serialize the intermediary pdf for embedding the signature later (to make sure no timestamps are altered, etc). But from the first SO question, it seems to be a harder problem than I thought. Can it actually be done?

I'm using pdfbox.

Server code:

        PDDocument document = PDDocument.load(documentFile);
        PDSignature signature = new PDSignature();
        signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
        signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
        signature.setName("Example User");
        signature.setLocation("Los Angeles, CA");
        signature.setReason("Testing");
        Calendar date = Calendar.getInstance();
        signature.setSignDate(date);
        document.addSignature(signature);

        ExternalSigningSupport externalSigningSupport = document.saveIncrementalForExternalSigning(null);

        byte[] content = IOUtils.toByteArray(externalSigningSupport.getContent());
        MessageDigest md = MessageDigest.getInstance("SHA256", new BouncyCastleProvider());
        byte[] digest = md.digest(content); // this is sent to client

What I'm basically doing is sending that digest to the client to sign and then on the server redoing the above steps and setting the client signature:

        ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(fos);
        externalSigning.setSignature(encodedSignature); // encodedSignature is received from client and computed based on the digest sent by the server

This setup ends up with the integrity of the file being corrupted, since I'm creating a new PDSignature once I have the encodedSignature on the server to embed it. Is there a way to serialize the PDDocument created after calling addSignature, so I can later deserialize it on the server and add the client's signature?


Answer:

What I'm basically doing is sending that digest to the client to sign and then on the server redoing the above steps and setting the client signature

If you want those above steps to generate identical documents, you need to

  • make sure the inputs to those steps are identical and
  • provide the same revision id seed value.

If you do so, the outputs of the above steps are identical as is required for your task.

Making sure the inputs are identical

One step of your above steps is prone to result in different inputs:

Calendar date = Calendar.getInstance();
signature.setSignDate(date);

To guarantee identical inputs, you have to determine date only once and use that single value every time you execute those steps for the same signing transaction.

Providing the same revision id seed value

As recommended by the specification, PDFBox attempts to give each PDF revision its unique ID. In the case at hand, though, we need the same revision ID both times the above steps are executed.

Fortunately, PDFBox allows us to provide the seed value it uses to make the revision ID unique enough.

As we don't want to same revision ID all the time we sign the same document but merely during the current signing transaction, we should use the same seed value only in the same transaction. As the seed value is a long, we can simply use the time in milliseconds corresponding to the date already discussed above, i.e.:

pdDocument.setDocumentId(date.getTimeInMillis());

Question:

In PDFBox 2.x I put /Lock dictionary to signature field:

import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;

public class SigningUtils {
    public static final COSName COS_NAME_LOCK = COSName.getPDFName("Lock");
    public static final COSName COS_NAME_ACTION = COSName.getPDFName("Action");
    public static final COSName COS_NAME_ALL = COSName.getPDFName("All");
    public static final COSName COS_NAME_SIG_FIELD_LOCK = COSName.getPDFName("SigFieldLock");

    public static void setLock(PDSignatureField pdSignatureField, PDAcroForm acroForm) {
        COSDictionary lockDict = new COSDictionary();
        lockDict.setItem(COS_NAME_ACTION, COS_NAME_ALL);
        lockDict.setItem(COSName.TYPE, COS_NAME_SIG_FIELD_LOCK);
        pdSignatureField.getCOSObject().setItem(COS_NAME_LOCK, lockDict);
    }
}

Then I sign the signature field:

PDSignature signature = findExistingSignature(document, signatureFieldName); //This is some method to find signature field and create PDSignature dictionary

signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);

signature.setName("blablabla");
signature.setLocation("blablabla");
signature.setReason("blablabla");
signature.setSignDate(Calendar.getInstance());
document.addSignature(signature, this);

Everything looks allright except that when I open signed document in Adobe Acrobat it complains the content of the document was changed. If I don't add the /Lock dictionary the everything is fine.

Anyone has any idea what is wrong?


Answer:

The problem is that PDFBox signing does not take the Lock dictionary into account.

According to ISO 32000-1 (and also similarly ISO 32000-2):

12.8.2.4 FieldMDP

The FieldMDP transform method shall be used to detect changes to the values of a list of form fields. The entries in its transform parameters dictionary are listed in Table 256.

[...]

  • The author can also specify that after a specific recipient has signed the document, any modifications to specific form fields shall invalidate that recipient’s signature. There shall be a separate signature field for each designated recipient, each having an associated signature field lock dictionary (see Table 233) specifying the form fields that shall be locked for that user.

  • When the recipient signs the field, the signature, signature reference, and transform parameters dictionaries shall be created. The Action and Fields entries in the transform parameters dictionary shall be copied from the corresponding fields in the signature field lock dictionary.

Thus, the expected handling of a signature Lock dictionary includes the addition of matching FieldMDP transform data to the signature field value. PDFBox signing does not do so by default.

You can manually do it like this during signing:

PDSignatureField signatureField = FIND_YOUR_SIGNATURE_FIELD_TO_SIGN;
PDSignature signature = new PDSignature();
signatureField.setValue(signature);

COSBase lock = signatureField.getCOSObject().getDictionaryObject(COSName.getPDFName("Lock"));
if (lock instanceof COSDictionary)
{
    COSDictionary lockDict = (COSDictionary) lock;
    COSDictionary transformParams = new COSDictionary(lockDict);
    transformParams.setItem(COSName.TYPE, COSName.getPDFName("TransformParams"));
    transformParams.setItem(COSName.V, COSName.getPDFName("1.2"));
    transformParams.setDirect(true);
    COSDictionary sigRef = new COSDictionary();
    sigRef.setItem(COSName.TYPE, COSName.getPDFName("SigRef"));
    sigRef.setItem(COSName.getPDFName("TransformParams"), transformParams);
    sigRef.setItem(COSName.getPDFName("TransformMethod"), COSName.getPDFName("FieldMDP"));
    sigRef.setItem(COSName.getPDFName("Data"), document.getDocumentCatalog());
    sigRef.setDirect(true);
    COSArray referenceArray = new COSArray();
    referenceArray.add(sigRef);
    signature.getCOSObject().setItem(COSName.getPDFName("Reference"), referenceArray);
}

signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("blablabla");
signature.setLocation("blablabla");
signature.setReason("blablabla");
signature.setSignDate(Calendar.getInstance());
document.addSignature(signature [, ...]);

(CreateSignature helper method signExistingFieldWithLock)


Concerning the P entry in the signature Lock dictionary discussed in the comments: This entry has been introduced in the Adobe supplement to ISO 32000, extension level 3.

Question:

As Adobe article "Digital Signatures in a PDF" stating:

PDF defines two types of signatures: approval and certification. The differences are as follows: Approval: There can be any number of approval signatures in a document. The field may optionally be associated with FieldMDP permissions. Certification: There can be only one certification signature and it must be the first one in a document. The field is always associated with DocMDP.

Using PDFBox examples I was able to successfully apply multiple signatures to my document: https://github.com/apache/pdfbox/blob/trunk/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature.java In order to apply multiple signatures I was just running same code multiple times with different signature placeholders and images.

But what I have distinguished is that, even though I am running same code it always sets first signature as Certified, and all other once as Approval.

But in my case, I don't want a document to be certified, I just need all signatures to be of Apploval type including the first one. I know I can invisible first Certifying signature, but still I do not want to certify document at all.

I was trying to find a way to setup signature, but couldn't figure it out.

Here is my usage of example code (other classes are in the GitHub link above):

public class SignnerPDFBoxExample extends CreateSignatureBase {

    private SignatureOptions signatureOptions;
    private PDVisibleSignDesigner visibleSignDesigner;
    private final PDVisibleSigProperties visibleSignatureProperties = new PDVisibleSigProperties();
    private boolean lateExternalSigning = false;

    public static void main(String[] args) throws Exception {

        File ksFile = new File("keystore.jks");
        KeyStore keystore = KeyStore.getInstance("JKS");
        char[] pin = "123456".toCharArray();
        keystore.load(new FileInputStream(ksFile), pin);

        SignnerPDFBoxExample signer = new SignnerPDFBoxExample(keystore, pin.clone());
        String inputFilename = "Four_Signature_template.pdf";

        File documentFile = new File(inputFilename);
        File signedDocumentFile;
        int page = 1;
        try (FileInputStream imageStream = new FileInputStream("client_signature.jpg"))
        {
            String name = documentFile.getName();
            String substring = name.substring(0, name.lastIndexOf('.'));
            signedDocumentFile = new File(documentFile.getParent(), substring + "_signed.pdf");
            // page is 1-based here
            signer.setVisibleSignDesigner(inputFilename, 0, 0, -50, imageStream, page);
        }
        signer.setVisibleSignatureProperties("name", "location", "Signed using PDFBox", 0, page, true);
        signer.signPDF(documentFile, signedDocumentFile, null, "certifySignature");
    }

    public boolean isLateExternalSigning()
    {
        return lateExternalSigning;
    }

    /**
     * Set late external signing. Enable this if you want to activate the demo code where the
     * signature is kept and added in an extra step without using PDFBox methods. This is disabled
     * by default.
     *
     * @param lateExternalSigning
     */
    public void setLateExternalSigning(boolean lateExternalSigning)
    {
        this.lateExternalSigning = lateExternalSigning;
    }

    /**
     * Set visible signature designer for a new signature field.
     * 
     * @param filename
     * @param x position of the signature field
     * @param y position of the signature field
     * @param zoomPercent
     * @param imageStream
     * @param page the signature should be placed on
     * @throws IOException
     */
    public void setVisibleSignDesigner(String filename, int x, int y, int zoomPercent, 
            FileInputStream imageStream, int page) 
            throws IOException
    {
        visibleSignDesigner = new PDVisibleSignDesigner(filename, imageStream, page);
        visibleSignDesigner.xAxis(x).yAxis(y).zoom(zoomPercent).adjustForRotation();
    }

    /**
     * Set visible signature designer for an existing signature field.
     * 
     * @param zoomPercent
     * @param imageStream
     * @throws IOException
     */
    public void setVisibleSignDesigner(int zoomPercent, FileInputStream imageStream) 
            throws IOException
    {
        visibleSignDesigner = new PDVisibleSignDesigner(imageStream);
        visibleSignDesigner.zoom(zoomPercent);
    }

    /**
     * Set visible signature properties for new signature fields.
     * 
     * @param name
     * @param location
     * @param reason
     * @param preferredSize
     * @param page
     * @param visualSignEnabled
     * @throws IOException
     */
    public void setVisibleSignatureProperties(String name, String location, String reason, int preferredSize, 
            int page, boolean visualSignEnabled) throws IOException
    {
        visibleSignatureProperties.signerName(name).signerLocation(location).signatureReason(reason).
                preferredSize(preferredSize).page(page).visualSignEnabled(visualSignEnabled).
                setPdVisibleSignature(visibleSignDesigner);
    }

    /**
     * Set visible signature properties for existing signature fields.
     * 
     * @param name
     * @param location
     * @param reason
     * @param visualSignEnabled
     * @throws IOException
     */
    public void setVisibleSignatureProperties(String name, String location, String reason,
            boolean visualSignEnabled) throws IOException
    {
        visibleSignatureProperties.signerName(name).signerLocation(location).signatureReason(reason).
                visualSignEnabled(visualSignEnabled).setPdVisibleSignature(visibleSignDesigner);
    }

    /**
     * Initialize the signature creator with a keystore (pkcs12) and pin that
     * should be used for the signature.
     *
     * @param keystore is a pkcs12 keystore.
     * @param pin is the pin for the keystore / private key
     * @throws KeyStoreException if the keystore has not been initialized (loaded)
     * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
     * @throws UnrecoverableKeyException if the given password is wrong
     * @throws CertificateException if the certificate is not valid as signing time
     * @throws IOException if no certificate could be found
     */
    public SignnerPDFBoxExample(KeyStore keystore, char[] pin)
            throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException
    {
        super(keystore, pin);
    }

    /**
     * Sign pdf file and create new file that ends with "_signed.pdf".
     *
     * @param inputFile The source pdf document file.
     * @param signedFile The file to be signed.
     * @param tsaClient optional TSA client
     * @throws IOException
     */
    public void signPDF(File inputFile, File signedFile, TSAClient tsaClient) throws IOException
    {
        this.signPDF(inputFile, signedFile, tsaClient, null);
    }

    /**
     * Sign pdf file and create new file that ends with "_signed.pdf".
     *
     * @param inputFile The source pdf document file.
     * @param signedFile The file to be signed.
     * @param tsaClient optional TSA client
     * @param signatureFieldName optional name of an existing (unsigned) signature field
     * @throws IOException
     */
    public void signPDF(File inputFile, File signedFile, TSAClient tsaClient, String signatureFieldName) throws IOException
    {
        setTsaClient(tsaClient);

        if (inputFile == null || !inputFile.exists())
        {
            throw new IOException("Document for signing does not exist");
        }

        // creating output document and prepare the IO streams.
        FileOutputStream fos = new FileOutputStream(signedFile);

        try (PDDocument doc = PDDocument.load(inputFile))
        {
            int accessPermissions = SigUtils.getMDPPermission(doc);
            if (accessPermissions == 1)
            {
                throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
            }
            // Note that PDFBox has a bug that visual signing on certified files with permission 2
            // doesn't work properly, see PDFBOX-3699. As long as this issue is open, you may want to
            // be careful with such files.

            PDSignature signature;

            // sign a PDF with an existing empty signature, as created by the CreateEmptySignatureForm example.
            signature = findExistingSignature(doc, signatureFieldName);

            if (signature == null)
            {
                // create signature dictionary
                signature = new PDSignature();
            }

            // Optional: certify
            // can be done only if version is at least 1.5 and if not already set
            // doing this on a PDF/A-1b file fails validation by Adobe preflight (PDFBOX-3821)
            // PDF/A-1b requires PDF version 1.4 max, so don't increase the version on such files.
            if (doc.getVersion() >= 1.5f && accessPermissions == 0)
            {
                SigUtils.setMDPPermission(doc, signature, 2);
            }

            PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
            if (acroForm != null && acroForm.getNeedAppearances())
            {
                // PDFBOX-3738 NeedAppearances true results in visible signature becoming invisible 
                // with Adobe Reader
                if (acroForm.getFields().isEmpty())
                {
                    // we can safely delete it if there are no fields
                    acroForm.getCOSObject().removeItem(COSName.NEED_APPEARANCES);
                    // note that if you've set MDP permissions, the removal of this item
                    // may result in Adobe Reader claiming that the document has been changed.
                    // and/or that field content won't be displayed properly.
                    // ==> decide what you prefer and adjust your code accordingly.
                }
                else
                {
                    System.out.println("/NeedAppearances is set, signature may be ignored by Adobe Reader");
                }
            }

            // default filter
            signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);

            // subfilter for basic and PAdES Part 2 signatures
            signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);

            if (visibleSignatureProperties != null)
            {
                // this builds the signature structures in a separate document
                visibleSignatureProperties.buildSignature();

                signature.setName(visibleSignatureProperties.getSignerName());
                signature.setLocation(visibleSignatureProperties.getSignerLocation());
                signature.setReason(visibleSignatureProperties.getSignatureReason());
            }

            // the signing date, needed for valid signature
            signature.setSignDate(Calendar.getInstance());

            // do not set SignatureInterface instance, if external signing used
            SignatureInterface signatureInterface = isExternalSigning() ? null : this;

            // register signature dictionary and sign interface
            if (visibleSignatureProperties != null && visibleSignatureProperties.isVisualSignEnabled())
            {
                signatureOptions = new SignatureOptions();
                signatureOptions.setVisualSignature(visibleSignatureProperties.getVisibleSignature());
                signatureOptions.setPage(visibleSignatureProperties.getPage() - 1);
                doc.addSignature(signature, signatureInterface, signatureOptions);
            }
            else
            {
                doc.addSignature(signature, signatureInterface);
            }

            if (isExternalSigning())
            {
                System.out.println("Signing externally " + signedFile.getName());
                ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
                // invoke external signature service
                byte[] cmsSignature = sign(externalSigning.getContent());

                // Explanation of late external signing (off by default):
                // If you want to add the signature in a separate step, then set an empty byte array
                // and call signature.getByteRange() and remember the offset signature.getByteRange()[1]+1.
                // you can write the ascii hex signature at a later time even if you don't have this
                // PDDocument object anymore, with classic java file random access methods.
                // If you can't remember the offset value from ByteRange because your context has changed,
                // then open the file with PDFBox, find the field with findExistingSignature() or
                // PODDocument.getLastSignatureDictionary() and get the ByteRange from there.
                // Close the file and then write the signature as explained earlier in this comment.
                if (isLateExternalSigning())
                {
                    // this saves the file with a 0 signature
                    externalSigning.setSignature(new byte[0]);

                    // remember the offset (add 1 because of "<")
                    int offset = signature.getByteRange()[1] + 1;

                    // now write the signature at the correct offset without any PDFBox methods
                    try (RandomAccessFile raf = new RandomAccessFile(signedFile, "rw"))
                    {
                        raf.seek(offset);
                        raf.write(Hex.getBytes(cmsSignature));
                    }
                }
                else
                {
                    // set signature bytes received from the service and save the file
                    externalSigning.setSignature(cmsSignature);
                }
            }
            else
            {
                // write incremental (only for signing purpose)
                doc.saveIncremental(fos);
            }
        }

        // Do not close signatureOptions before saving, because some COSStream objects within
        // are transferred to the signed document.
        // Do not allow signatureOptions get out of scope before saving, because then the COSDocument
        // in signature options might by closed by gc, which would close COSStream objects prematurely.
        // See https://issues.apache.org/jira/browse/PDFBOX-3743
        IOUtils.closeQuietly(signatureOptions);
    }

    // Find an existing signature (assumed to be empty). You will usually not need this.
    private PDSignature findExistingSignature(PDDocument doc, String sigFieldName)
    {
        PDSignature signature = null;
        PDSignatureField signatureField;
        PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
        if (acroForm != null)
        {
            signatureField = (PDSignatureField) acroForm.getField(sigFieldName);
            if (signatureField != null)
            {
                // retrieve signature dictionary
                signature = signatureField.getSignature();
                if (signature == null)
                {
                    signature = new PDSignature();
                    // after solving PDFBOX-3524
                    // signatureField.setValue(signature)
                    // until then:
                    signatureField.getCOSObject().setItem(COSName.V, signature);
                }
                else
                {
                    throw new IllegalStateException("The signature field " + sigFieldName + " is already signed.");
                }
            }
        }
        return signature;
    }

    /**
     * This will print the usage for this program.
     */
    private static void usage()
    {
        System.err.println("Usage: java " + CreateVisibleSignature.class.getName()
                + " <pkcs12-keystore-file> <pin> <input-pdf> <sign-image>\n" + "" +
                           "options:\n" +
                           "  -tsa <url>    sign timestamp using the given TSA server\n"+
                           "  -e            sign using external signature creation scenario");
    }


}

Answer:

Your signPDF method contains this code:

        // Optional: certify
        // can be done only if version is at least 1.5 and if not already set
        // doing this on a PDF/A-1b file fails validation by Adobe preflight (PDFBOX-3821)
        // PDF/A-1b requires PDF version 1.4 max, so don't increase the version on such files.
        if (doc.getVersion() >= 1.5f && accessPermissions == 0)
        {
            SigUtils.setMDPPermission(doc, signature, 2);
        }

If you don't want a certification signature to start with, remove this setMDPPermission call.

Question:

Is it possible to add additional page to the signed PDF and sign it again without breaking the first signature.

I read in the adobe documentation under incremental updates that it may be possible.

However, I'm not sure if that applies to all the content or just the Annotations (commenting), form fill-in and digital signatures.

I tried to do this by using Apache PDFBox in Java, by signing the document, then loading it, appending the page to it, saving it using saveIncremental() and signing it again.

However, the first signature gets invalidated.

Here is my generateTest method that generates the new PDF:

public byte[] generateTest(InputStream requestPdfIn) throws IOException {

    // Create a document and add a page to it
    // PDDocument document = new PDDocument();
    PDDocument document = PDDocument.load(requestPdfIn);
    PDPage page = new PDPage(PDRectangle.A4);
    document.addPage(page);     

    COSBase item = document.getPages().getCOSObject().getItem(COSName.KIDS);
    ((COSUpdateInfo) item).setNeedToBeUpdated(true);
    COSArray kids = (COSArray) item;
    kids.setNeedToBeUpdated(true);
    ((COSUpdateInfo) kids.get(0)).setNeedToBeUpdated(true);

    document.getPage(0).getCOSObject().setNeedToBeUpdated(true);
    page.getCOSObject().setNeedToBeUpdated(true);
    document.getPages().getCOSObject().setNeedToBeUpdated(true);

    COSDictionary dict = page.getCOSObject();
    while (dict.containsKey(COSName.PARENT)) {
        COSBase parent = dict.getDictionaryObject(COSName.PARENT);
        if (parent instanceof COSDictionary) {
            dict = (COSDictionary) parent;
            dict.setNeedToBeUpdated(true);
        }
    }

    document.getDocumentCatalog().getCOSObject().setNeedToBeUpdated(true);
    //document.getDocumentCatalog().getStructureTreeRoot().getCOSObject().setNeedToBeUpdated(true);

    // Save the results and ensure that the document is properly closed:
    ByteArrayOutputStream confirmationPdfOut = new ByteArrayOutputStream();
    document.saveIncremental(confirmationPdfOut);
    document.close();

    return confirmationPdfOut.toByteArray();

}

I found in this post that all the COSObjects need to have a flag needToBeUpdated set to true.

However, that still doesn't help when trying to add another page to the document, as the first signature gets invalidated when I try to verify the signature using Acrobat Reader.

Is it even possible? Is it possible with the PDFBox?


Answer:

No, it is not possible. Adding pages to an already signed PDF is not allowed.

In detail

I read in the adobe documentation under incremental updates that it may be possible.

Indeed it is possible to add changes to a PDF without touching the former revision. Thus, if that former revision was signed, the signature mathematically remains valid, it still signs the correct hash value.

But the PDF specification and its main interpretation (by Adobe that is) contain additional restrictions, cf. this stack overflow answer. As you find there at most the following changes are allowed to signed documents:

  • Adding signature fields
  • Adding or editing annotations
  • Supplying form field values
  • Digitally signing

At least Adobe Acrobat (Reader) does test for such changes in addition to the check for mathematical validity even if numerous other validation services don't.

Thus, your task to add additional page to the signed PDF and sign it again without breaking the first signature cannot be implemented.

Question:

How can we revert last incremental update done in a pdf using pdfbox ?

For e.g. Original document Signed document

When I digitally sign(certification signature) an original document using incremental save, I get a signed document. Upon inspecting the source of signed document, I could see that "%%EOF" is presenting 2 times. If I manually remove last "%%EOF" along with its content, I could see PDF returns to its initial state, which is very similar to original document.

How can I do this pragmatically ?

I am using PDFBOX v2.0.8

Best Regards, Abhishek


Answer:

There are more advanced approaches and there are less advanced ones.

This is the most simple one: It searches the %%EOF marker and cuts off right thereafter. This might not be identical to the original previous revision because that marker may be followed by an optional end-of-line marker. Unless that previous revision is signed or linearized, though, the variant with the end-of-line marker and the one without are equivalent as PDF files.

For searching the %%EOF marker we use the StreamSearcher class from the twitter/elephant-bird project, cf. this earlier stack overflow answer:

public List<Long> simpleApproach(InputStream pdf) throws IOException {
    StreamSearcher streamSearcher = new StreamSearcher("%%EOF".getBytes());
    List<Long> results = new ArrayList<>();
    long revisionSize = 0;
    long diff;
    while ((diff = streamSearcher.search(pdf)) > -1) {
        revisionSize += diff;
        results.add(revisionSize);
    }
    return results;
}

For copying only the desired number of bytes, we use the Guava ByteStreams class. (There are many alternatives, e.g. Apache Commons IO, but Guava happened to already be in my test project dependencies.)

List<Long> simpleSizes = null;
try (   InputStream resource = GET_DOCUMENT_INPUTSTREAM) {
    simpleSizes = simpleApproach(resource);
}

if (1 < simpleSizes.size()) {
    try (   InputStream resource = GET_DOCUMENT_INPUTSTREAM;
            OutputStream file = new FileOutputStream("previousRevision.pdf")) {
        InputStream revision = ByteStreams.limit(resource, simpleSizes.get(simpleSizes.size() - 2));
        ByteStreams.copy(revision, file);
    }
}

GET_DOCUMENT_INPUTSTREAM might be a new FileInputStream(PDF_PATH) or new ByteArrayInputStream(PDF_BYTES) or whatever means you have to repeatedly retrieve an InputStream for the PDF. In case of these examples (FileInputStream, ByteArrayInputStream) you can even re-use the same stream using reset().

Question:

I have loaded a PDDocument.

I retrieved the PDSignature object named sig.

The byte range of the signature is provided by sig.getByteRange(). In my case it is:

0-18373 43144-46015

I want to verify that the byte range of the signature is valid. Because the signature has to verify the whole file expect itself. Also the byte range is provided by the signature so I cannot rely on it.

I can check the first value to be 0 and the last value has to be the size of the file -1.

But I also need to verify the second and the third value (18373 and 43144). Therefore I need to know the position of the PDSignature in the document and its length.

How do I get these?


Answer:

Have a look at the PDFBox example ShowSignature. It does this indirectly: It checks whether the bytes in the gap of the byte ranges coincide exactly with the signature value determined by document parsing.

In the method showSignature:

int[] byteRange = sig.getByteRange();
if (byteRange.length != 4)
{
    System.err.println("Signature byteRange must have 4 items");
}
else
{
    long fileLen = infile.length();
    long rangeMax = byteRange[2] + (long) byteRange[3];
    // multiply content length with 2 (because it is in hex in the PDF) and add 2 for < and >
    int contentLen = contents.getString().length() * 2 + 2;
    if (fileLen != rangeMax || byteRange[0] != 0 || byteRange[1] + contentLen != byteRange[2])
    {
        // a false result doesn't necessarily mean that the PDF is a fake
        // see this answer why:
        // https://stackoverflow.com/a/48185913/535646
        System.out.println("Signature does not cover whole document");
    }
    else
    {
        System.out.println("Signature covers whole document");
    }
    checkContentValueWithFile(infile, byteRange, contents);
}

The helper method checkContentValueWithFile:

private void checkContentValueWithFile(File file, int[] byteRange, COSString contents) throws IOException
{
    // https://stackoverflow.com/questions/55049270
    // comment by mkl: check whether gap contains a hex value equal
    // byte-by-byte to the Content value, to prevent attacker from using a literal string
    // to allow extra space
    try (RandomAccessBufferedFileInputStream raf = new RandomAccessBufferedFileInputStream(file))
    {
        raf.seek(byteRange[1]);
        int c = raf.read();
        if (c != '<')
        {
            System.err.println("'<' expected at offset " + byteRange[1] + ", but got " + (char) c);
        }
        byte[] contentFromFile = raf.readFully(byteRange[2] - byteRange[1] - 2);
        byte[] contentAsHex = Hex.getString(contents.getBytes()).getBytes(Charsets.US_ASCII);
        if (contentFromFile.length != contentAsHex.length)
        {
            System.err.println("Raw content length from file is " +
                    contentFromFile.length +
                    ", but internal content string in hex has length " +
                    contentAsHex.length);
        }
        // Compare the two, we can't do byte comparison because of upper/lower case
        // also check that it is really hex
        for (int i = 0; i < contentFromFile.length; ++i)
        {
            try
            {
                if (Integer.parseInt(String.valueOf((char) contentFromFile[i]), 16) !=
                    Integer.parseInt(String.valueOf((char) contentAsHex[i]), 16))
                {
                    System.err.println("Possible manipulation at file offset " +
                            (byteRange[1] + i + 1) + " in signature content");
                    break;
                }
            }
            catch (NumberFormatException ex)
            {
                System.err.println("Incorrect hex value");
                System.err.println("Possible manipulation at file offset " +
                        (byteRange[1] + i + 1) + " in signature content");
                break;
            }
        }
        c = raf.read();
        if (c != '>')
        {
            System.err.println("'>' expected at offset " + byteRange[2] + ", but got " + (char) c);
        }
    }
}

(Strictly speaking a binary string in normal brackets would also be ok as long as it fills the whole gap, it needn't be a hex string.)

Question:

I am trying to sign a PDF with 2 signature fields using the example code provided by PDFBox (https://svn.apache.org/repos/asf/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature.java). But the signed PDF shows There have been changes made to this document that invalidate the signature.

I have uploaded my sample project to GitHub please find it here.

The project can be opened using IntelliJ or Eclipse.

The program argument should be set to the following to simulate the problem.

keystore/lawrence.p12 12345678 pdfs/Fillable-2.pdf images/image.jpg

Grateful if any PDFBox expert can help me. Thank you.


Answer:

This answer to the question "Lock" dictionary in signature field is the reason of broken signature after signing already contains code for signing that respects the signature Lock dictionary and creates a matching FieldMDP transformations while signing.

As clarified in a comment, though, the OP wonders

is there any way to lock the corresponding textfield after signing

Thus, not only shall changes to protected form fields invalidate the signature in question but in the course of signing these protected fields shall themselves be locked.

Indeed, one can improve the code from the referenced answer to do that, too:

PDSignatureField signatureField = FIND_YOUR_SIGNATURE_FIELD_TO_SIGN;
PDSignature signature = new PDSignature();
signatureField.setValue(signature);

COSBase lock = signatureField.getCOSObject().getDictionaryObject(COS_NAME_LOCK);
if (lock instanceof COSDictionary)
{
    COSDictionary lockDict = (COSDictionary) lock;
    COSDictionary transformParams = new COSDictionary(lockDict);
    transformParams.setItem(COSName.TYPE, COSName.getPDFName("TransformParams"));
    transformParams.setItem(COSName.V, COSName.getPDFName("1.2"));
    transformParams.setDirect(true);
    COSDictionary sigRef = new COSDictionary();
    sigRef.setItem(COSName.TYPE, COSName.getPDFName("SigRef"));
    sigRef.setItem(COSName.getPDFName("TransformParams"), transformParams);
    sigRef.setItem(COSName.getPDFName("TransformMethod"), COSName.getPDFName("FieldMDP"));
    sigRef.setItem(COSName.getPDFName("Data"), document.getDocumentCatalog());
    sigRef.setDirect(true);
    COSArray referenceArray = new COSArray();
    referenceArray.add(sigRef);
    signature.getCOSObject().setItem(COSName.getPDFName("Reference"), referenceArray);

    final Predicate<PDField> shallBeLocked;
    final COSArray fields = lockDict.getCOSArray(COSName.FIELDS);
    final List<String> fieldNames = fields == null ? Collections.emptyList() :
        fields.toList().stream().filter(c -> (c instanceof COSString)).map(s -> ((COSString)s).getString()).collect(Collectors.toList());
    final COSName action = lockDict.getCOSName(COSName.getPDFName("Action"));
    if (action.equals(COSName.getPDFName("Include"))) {
        shallBeLocked = f -> fieldNames.contains(f.getFullyQualifiedName());
    } else if (action.equals(COSName.getPDFName("Exclude"))) {
        shallBeLocked = f -> !fieldNames.contains(f.getFullyQualifiedName());
    } else if (action.equals(COSName.getPDFName("All"))) {
        shallBeLocked = f -> true;
    } else { // unknown action, lock nothing
        shallBeLocked = f -> false;
    }
    lockFields(document.getDocumentCatalog().getAcroForm().getFields(), shallBeLocked);
}

signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("blablabla");
signature.setLocation("blablabla");
signature.setReason("blablabla");
signature.setSignDate(Calendar.getInstance());
document.addSignature(signature [, ...]);

(CreateSignature helper method signAndLockExistingFieldWithLock)

with lockFields implemented like this:

boolean lockFields(List<PDField> fields, Predicate<PDField> shallBeLocked) {
    boolean isUpdated = false;
    if (fields != null) {
        for (PDField field : fields) {
            boolean isUpdatedField = false;
            if (shallBeLocked.test(field)) {
                field.setFieldFlags(field.getFieldFlags() | 1);
                if (field instanceof PDTerminalField) {
                    for (PDAnnotationWidget widget : ((PDTerminalField)field).getWidgets())
                        widget.setLocked(true);
                }
                isUpdatedField = true;
            }
            if (field instanceof PDNonTerminalField) {
                if (lockFields(((PDNonTerminalField)field).getChildren(), shallBeLocked))
                    isUpdatedField = true;
            }
            if (isUpdatedField) {
                field.getCOSObject().setNeedToBeUpdated(true);
                isUpdated = true;
            }
        }
    }
    return isUpdated;
}

(CreateSignature helper method lockFields)

Question:

I'm working on a project used for signing PDF files via Adobe AATL certificates and I'm using the PDFBOX library. My code below is working for files that are smaller than 4MB and breaks for files that are bigger.

NOTE: the code below does not throw an exception. However, the file that it signed is not opening due to integrity issue.

public void signDetached(PDDocument document, File inFile, SignProperties sigProps)
        throws IOException {
    int accessPermissions = SigUtils.getMDPPermission(document);
    if (accessPermissions == 1) {
        throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
    }

    // create signature dictionary
    PDSignature signature = new PDSignature();
    signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
    signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
    signature.setName(sigProps.getName());
    signature.setLocation(sigProps.getLocation());
    signature.setReason(sigProps.getReason());

    // the signing date, needed for valid signature
    signature.setSignDate(Calendar.getInstance());

    // Optional: certify 
    if (accessPermissions == 0)
        SigUtils.setMDPPermission(document, signature, 2);

    SignatureOptions signatureOptions = new SignatureOptions();
    // Size can vary, but should be enough for purpose.
    signatureOptions.setPreferredSignatureSize(SignatureOptions.DEFAULT_SIGNATURE_SIZE * 3);
    // register signature dictionary and sign interface
    document.addSignature(signature, this, signatureOptions);

    FileOutputStream stream = new FileOutputStream(inFile);

    // write incremental (only for signing purpose)
    document.saveIncremental(stream);
    document.close();
    stream.close();
}

UPDATE: PDFBOX version is 2.0.8 Input File anything bigger than 4 MB


Answer:

This code of yours

FileOutputStream stream = new FileOutputStream(inFile);
// write incremental (only for signing purpose)
document.saveIncremental(stream);

writes into the input file. Don't do that (the javadoc warns about this) because PDFBox also reads from that file when calculating the signature while also writing into it... this results in a really nasty mess. So always save into a different file when signing. And update to the current version.

Question:

I was trying to add multiple signatures in a single pdf on stamper. I am able to add multiple stampers. In my case on one, I was getting the error

at least one signature is invalid.

I want to add multiple valid signs in a single PDF. Please help me. In image only one sign is valid other signs are invalid, so let me what I'm doing wrong

My code snapshot below

public void getSignOnPdf(Map<Integer, byte[]> PdfSigneture1, List<Long> documentIds, List<String> calTimeStamp,
        String originalPdfReadServerPath, String tickImagePath, int serverTime, int pageNumberToInsertStamp,
        String name, String location, String reasonForSign, int xCo_ordinates, int yCo_ordinates,
        int signatureWidth, int signatureHeight, String pdfPassword, String internal_outputFinalPdfPath)
        throws Exception {
    String pdfReadServerPath = null;
    String l_slash = new String();
    String originalPDFPath = new String(originalPdfReadServerPath.trim());

    boolean isCorrectPDFOutputPath = false;
    String aspOutputPdfServerPath = null;
    synchronized (this) {
        if ((internal_outputFinalPdfPath != null) && (!internal_outputFinalPdfPath.trim().isEmpty())) {
            System.out.println("[" + EsignCommonFuntion.generateTimeStampForLog()
                    + "] :1-->  outputFinalPdfPath is: " + internal_outputFinalPdfPath);
            if (!(new File(internal_outputFinalPdfPath)).isFile()) {
                isCorrectPDFOutputPath = true;
                aspOutputPdfServerPath = internal_outputFinalPdfPath;
            } else {
                System.out.println("1--> Please provide directory path for outputFinalPdfPath: "
                        .concat(String.valueOf(internal_outputFinalPdfPath)));
            }
        } else {
            System.out.println(" 1--> outputFinalPdfPath is empty or null: "
                    .concat(String.valueOf(internal_outputFinalPdfPath)));
        }
    }
    boolean isPasswordPresent = false;
    String pdfPasswordForEncryption;
    synchronized (this) {
        if ((pdfPassword != null) && (!pdfPassword.trim().isEmpty())) {
            pdfPasswordForEncryption = pdfPassword.trim();
            isPasswordPresent = true;
        } else {
            pdfPasswordForEncryption = null;
        }
        String pdfOriginalName = (new File(originalPDFPath)).getName();
        String pdfAbsolutePath = originalPDFPath.substring(0, originalPDFPath.lastIndexOf(l_slash));
        if (isPasswordPresent) {
            pdfAbsolutePath = getEncryptedPdfName(originalPDFPath, pdfAbsolutePath + l_slash,
                    pdfPasswordForEncryption, pdfOriginalName);
            pdfReadServerPath = new String(pdfAbsolutePath);
        } else {
            pdfReadServerPath = originalPDFPath;
        }
    }
    ArrayList<String> unSignedFilesList = new ArrayList<String>();

    Map<Integer, byte[]> l_PdfSigneture = PdfSigneture1;

    int actual_pageNumForStamp = 1;

    String pdfFileName = (new File(pdfReadServerPath)).getName();

    FileOutputStream fos = null;

    String nameToShowInSignature = name;
    String locationToShowInSignature = location;
    String reasonForSignatureSign = reasonForSign;

    PDDocument documentFinal = null;
    try {
        pdfReadServerPath = pdfReadServerPath.substring(0, pdfReadServerPath.lastIndexOf(l_slash));
        System.out.println("inside getSignOnMethod pdfAbsolutePath:".concat(String.valueOf(pdfReadServerPath)));
        unSignedFilesList.add(pdfFileName);
        System.out.println("inside getSignOnMethod pdfFileName:".concat(String.valueOf(pdfFileName)));

        String PDFpath = pdfReadServerPath + l_slash + (String) (unSignedFilesList).get(0);

        System.out.println("Inside for PDFpath: ".concat(String.valueOf(PDFpath)));

        String finalOutputPdfName = ((String) (unSignedFilesList).get(0)).substring(0,
                ((String) (unSignedFilesList).get(0)).lastIndexOf(".")) + "_signedFinal.pdf";

        File outFile2 = null;

        if (isCorrectPDFOutputPath) {
            System.out.println("if condition Final signed PDF ouptut Path: " + aspOutputPdfServerPath + l_slash
                    + finalOutputPdfName);
            outFile2 = new File(aspOutputPdfServerPath + l_slash + finalOutputPdfName);
            fos = new FileOutputStream(outFile2);
        } else {
            outFile2 = new File(pdfReadServerPath + l_slash + outFile2);
            fos = new FileOutputStream(outFile2);
        }

        documentFinal = PDDocument.load(new File(PDFpath));

        for (int i = 1; i < 4; i++) {
            FileInputStream image2 = new FileInputStream(tickImagePath);

            PDSignature pdsignature = new PDSignature();
            pdsignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            pdsignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);

            Calendar cal = GregorianCalendar.getInstance();
            SimpleDateFormat l_simpleDateFormater = new SimpleDateFormat("yyyyMMdd_HHmmss");
            String timeStamp = (String) calTimeStamp.get(i - 1);

            try {
                cal.setTime(l_simpleDateFormater.parse(timeStamp));
            } catch (ParseException ex) {
                ex.printStackTrace();
            }

            cal.add(12, serverTime);
            pdsignature.setSignDate(cal);
            documentFinal.setDocumentId((Long) documentIds.get(i - 1));

            String dateToShowInSignature = cal.getTime().toString();

            Float saveIncrementalObj1 = null;
            saveIncrementalObj1 = new Float((float) xCo_ordinates, (float) yCo_ordinates, (float) signatureWidth,
                    (float) signatureHeight);

            PDRectangle rect = getPDRectangle(documentFinal, saveIncrementalObj1, i);
            PDVisibleSignDesigner visibleSig;
            (visibleSig = new PDVisibleSignDesigner(documentFinal, image2, i)).xAxis(xCo_ordinates)
                    .yAxis(yCo_ordinates).zoom(-95.0F).signatureFieldName("signature");

            PDVisibleSigProperties visibleSignatureProp = new PDVisibleSigProperties();

            visibleSignatureProp.signerName("name").signerLocation("location").signatureReason("Security")
                    .preferredSize(0).page(i - 1).visualSignEnabled(true).setPdVisibleSignature(visibleSig)
                    .buildSignature();
            try {
                PdfSigneture = new TreeMap<>();
                // PdfSigneture.clear();
                PdfSigneture = l_PdfSigneture;

                if (visibleSignatureProp.isVisualSignEnabled()) {
                    this.options = new SignatureOptions();
                    this.options.setVisualSignature(visibleSignatureProp);
                    this.options.setPage(visibleSignatureProp.getPage());
                    this.options.setVisualSignature(
                            getInputStream(documentFinal, i, rect, tickImagePath, nameToShowInSignature,
                                    locationToShowInSignature, dateToShowInSignature, reasonForSignatureSign));
                    documentFinal.addSignature(pdsignature, this, this.options);
                } else {
                    documentFinal.addSignature(pdsignature, this);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        synchronized (this) {
            SaveIncrementalSignObject saveIncrementalSignObject;
            (saveIncrementalSignObject = new SaveIncrementalSignObject()).setFos(fos);
            saveIncrementalSignObject.setPDDocumentFromFile(documentFinal);

            saveIncrementalForSign(saveIncrementalSignObject);
        }
    } catch (Exception localException2) {
        System.out.println("Insidemethod -- Exception block" + localException2.getMessage());
        return;
    } finally {
        fos.flush();
        if (fos != null) {
            fos.close();
        }
        documentFinal.close();
    }
}

public static synchronized void saveIncrementalForSign(SaveIncrementalSignObject p_SaveIncrementalObj) {
    PDDocument documentFinal = null;
    try {
        (documentFinal = p_SaveIncrementalObj.getPDDocumentFromFile())
                .saveIncremental(p_SaveIncrementalObj.getFos());
    } catch (Exception e) {
        e.printStackTrace();
        try {
        //              documentFinal.close();
            return;
        } catch (Exception eX) {
            eX.printStackTrace();
            return;
        }
    }
}

Answer:

In a comment you clarified what you want to achieve:

I tried to applying one signature to multiple place.

As discussed in the first section below this is not what your code does: your code attempts to apply multiple signatures to one place each in a single revision which is impossible as also explained there.

Applying a single signature to multiple places in a single revision, on the other hand, is not desired by the PDF specification team and some approaches to implement this have been made invalid by the specification, but it is possible as explained in the second section below.

Your approach, and why it cannot work

You appear to try to apply multiple signatures in one pass:

if (isPasswordPresent) {
    documentFinal = PDDocument.load(new File(PDFpath), pdfPasswordForEncryption);
} else {
    documentFinal = PDDocument.load(new File(PDFpath));
}

for (int i = 1; i < 4; i++) {
    FileInputStream image2 = new FileInputStream(tickImagePath);

    PDSignature pdsignature = new PDSignature();

    [...]

    try {
        [...]

        if (visibleSignatureProp.isVisualSignEnabled()) {
            [...]
            documentFinal.addSignature(pdsignature, this, this.options);
        } else {
            documentFinal.addSignature(pdsignature, this);
        }
    } catch (Exception e) {
        System.out.println("Inside getSignOnPdf sub exception block at addSignature:" + e + "error :" + e.getMessage());
        e.printStackTrace();
    }
}

synchronized (this) {
    [...]
    saveIncrementalForSign(saveIncrementalSignObject);
}

This cannot work.

In PDFs multiple signatures are applied one after the other in separate PDF revisions, not all in parallel in the same revision:

You can find some backgrounds in this answer and the documents referenced from there.

Thus, in pseudo code what you have to do instead is:

for (int i = 1; i < 4; i++) {
    load current version of the PDF;
    apply the i'th signature;
    save and sign as new current version of the PDF;
}

The method name PDDocument.addSignature might be a little misleading here as it might be assumed to imply that multiple signatures may be added. This is not the case; all signatures will be created as signature fields with their widgets but only the field of the last added PDSignature will actually be signed, so only this last added signature field will actually have a sensible value.

@Tilman - there probably should be a test in PDDocument.addSignature throwing an exception if a signature already has been added since loading the document.

A discussion of your actual task

The path of PDF objects from a signature visualization on a PDF page to the actual signature (the CMS signature container in case of CMS based subfilters) is not immediate. Instead we have

  • the PDF page, in its annotations referencing
  • the signature field widget (the signature visualization) belonging to
  • the signature field referencing
  • the signature value dictionary into which the CMS signature container is embedded.

For the implementation of your actual task,

applying one signature to multiple places,

therefore, there appear to be a number of options to get from multiple pages with signature appearances to the single signature container:

  1. All pages with signature visualizations pointing to the same single widget annotation of the single signature field with the value dictionary containing the signature container.
  2. Each page with signature visualizations pointing to their own widget, but all widgets belonging to the same single signature field with the value dictionary containing the signature container.
  3. Each page with signature visualizations pointing to their own widget, each widget belonging to a separate signature field, but all of them pointing to the same value dictionary containing the signature container.

Let's now look at the PDF specification ISO 32000-2. First of all it warns against having single signatures with multiple visualizations:

The location of a signature within a document can have a bearing on its legal meaning. [...]

If more than one location is associated with a signature, the meaning can become ambiguous.

(ISO 32000-2, section 12.7.5.5 "Signature fields")

Consequentially, the specification attempts to forbid single signatures with multiple visualizations:

A given annotation dictionary shall be referenced from the Annots array of only one page.

(ISO 32000-2, section 12.5.2 "Annotation dictionaries")

This forbids option 1 above.

signature fields shall never refer to more than one annotation

(ISO 32000-2, section 12.7.5.5 "Signature fields")

This forbids option 2.

Apparently, though, option 3 is not explicitly forbidden. For generic form fields value object sharing is even explicitly allowed as the form field value is inheritable!

Thus, strictly speaking creating signatures with multiple visualizations is possible using option 3.

Please be aware, though, that it clearly was not intended by the PDF specification team to allow them, it most likely was an oversight. Thus, you have to reckon that some upcoming corrigenda to the specification will eventually forbid option 3, too.

If you want to try nonetheless, it should be possible to tweak or patch PDFBox to create single signatures with multiple visualizations using the approach of option 3.

It has already proven possible for e.g. iText, cf. this answer.

Furthermore, the sample document you shared makes use of this option.

A proof of concept

As it turns out, it is pretty easy to create a multi-visualization PDF signature using PDFBox along the lines of option 3. In particular it is easier than doing this with iText, cf. the answer referenced above, because the signature value dictionary here is an object one creates and handles oneself while in iText it is created under the hood and just in time.

All one has to do is to create one PDSignature object and generate one signature with it normally (using PDDocument.addSignature) and then add as many other signature fields as one wants, setting the signature value properties of those fields to the single PDSignature object create at the start.

E.g. you can use a method like this to add additional signature fields:

void addSignatureField(PDDocument pdDocument, PDPage pdPage, PDRectangle rectangle, PDSignature signature) throws IOException {
    PDAcroForm acroForm = pdDocument.getDocumentCatalog().getAcroForm();
    List<PDField> acroFormFields = acroForm.getFields();

    PDSignatureField signatureField = new PDSignatureField(acroForm);
    signatureField.setSignature(signature);
    PDAnnotationWidget widget = signatureField.getWidgets().get(0);
    acroFormFields.add(signatureField);

    widget.setRectangle(rectangle);
    widget.setPage(pdPage);

    // from PDVisualSigBuilder.createHolderForm()
    PDStream stream = new PDStream(pdDocument);
    PDFormXObject form = new PDFormXObject(stream);
    PDResources res = new PDResources();
    form.setResources(res);
    form.setFormType(1);
    PDRectangle bbox = new PDRectangle(rectangle.getWidth(), rectangle.getHeight());
    float height = bbox.getHeight();

    form.setBBox(bbox);
    PDFont font = PDType1Font.HELVETICA_BOLD;

    // from PDVisualSigBuilder.createAppearanceDictionary()
    PDAppearanceDictionary appearance = new PDAppearanceDictionary();
    appearance.getCOSObject().setDirect(true);
    PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
    appearance.setNormalAppearance(appearanceStream);
    widget.setAppearance(appearance);

    try (PDPageContentStream cs = new PDPageContentStream(pdDocument, appearanceStream))
    {
        // show background (just for debugging, to see the rect size + position)
        cs.setNonStrokingColor(Color.yellow);
        cs.addRect(-5000, -5000, 10000, 10000);
        cs.fill();

        float fontSize = 10;
        float leading = fontSize * 1.5f;
        cs.beginText();
        cs.setFont(font, fontSize);
        cs.setNonStrokingColor(Color.black);
        cs.newLineAtOffset(fontSize, height - leading);
        cs.setLeading(leading);
        cs.showText("Signature text");
        cs.newLine();
        cs.showText("some additional Information");
        cs.newLine();
        cs.showText("let's keep talking");
        cs.endText();
    }

    pdPage.getAnnotations().add(widget);

    COSDictionary pageTreeObject = pdPage.getCOSObject(); 
    while (pageTreeObject != null) {
        pageTreeObject.setNeedToBeUpdated(true);
        pageTreeObject = (COSDictionary) pageTreeObject.getDictionaryObject(COSName.PARENT);
    }
}

(CreateMultipleVisualizations helper method)

(This method actually is based on the CreateVisibleSignature2.createVisualSignatureTemplate method from the pdfbox examples artifact but severely simplified and now used to create the actual signature fields, not merely a template to copy from.)

Used like this

try (   InputStream resource = PDF_SOURCE_STREAM;
        OutputStream result = PDF_TARGET_STREAM;
        PDDocument pdDocument = PDDocument.load(resource)   )
{
    PDAcroForm acroForm = pdDocument.getDocumentCatalog().getAcroForm();
    if (acroForm == null) {
        pdDocument.getDocumentCatalog().setAcroForm(acroForm = new PDAcroForm(pdDocument));
    }
    acroForm.setSignaturesExist(true);
    acroForm.setAppendOnly(true);
    acroForm.getCOSObject().setDirect(true);

    PDRectangle rectangle = new PDRectangle(100, 600, 300, 100);
    PDSignature signature = new PDSignature();
    signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
    signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
    signature.setName("Example User");
    signature.setLocation("Los Angeles, CA");
    signature.setReason("Testing");
    signature.setSignDate(Calendar.getInstance());
    pdDocument.addSignature(signature, this);

    for (PDPage pdPage : pdDocument.getPages()) {
        addSignatureField(pdDocument, pdPage, rectangle, signature);
    }

    pdDocument.saveIncremental(result);
}

(CreateMultipleVisualizations test testCreateSignatureWithMultipleVisualizations)

one retrieves a PDF with a signature visualization on each page of the result document (and an extra invisible one because I was a bit lazy) but only a single actual signature value (given that this implements SignatureInterface with the byte[] sign(InputStream) method).

Beware, though:

  • The PDSignatureField method setSignature has been deprecated in PDFBox 3.0.0-SNAPSHOT. You might eventually have to inject the PDSignature object using more low-level techniques.
  • This kind of multi-visualization signature is not wanted by the PDF specification teams. Chances are that they eventually will be forbidden.

Question:

Using itext I can get the signing name (Signed By) like this:

fields = reader.getAcroFields();

pk = fields.verifySignature(FieldName);

name = pk.getSignName();

How do I get the signing name using pdfBox?


Answer:

document.getSignatureDictionaries() gets the signatures, and PDSignature.getName() gets the name. To see more, have a look at the ShowSignature.java example from the source code download in the examples subproject.

Question:

the title says it all, I am able to visually sign a pdf using pdfbox version 2.0.8. Currently I have to hard-code the starting coordination of image in code. but as PDFs varies position of image always needs to be changed accordingly. I want to apply signature image at the end of pdf in left corner. how do I get that position in code? here is my code, hard coding coordinates using _x & _y. In code 'signing' is visible signature object and 'page' is the last page of pdf, 'args[2]' is pdf-file to be signed:

int _x = 30;
int _y = 420;
signing.setVisibleSignDesigner(args[2], _x, _y, -50, imageStream, page);
imageStream.close();
signing.setExternalSigning(externalSig);
signing.signPDF(documentFile, signedDocumentFile, tsaClient);
removeFile(imageResult);

Example of Signature I want:

Edit: added Image to clarify that I want signature field to be at the end of document, not at the end of last page. document may be completed at the top of the last page so field should also be right after the text not at the end of the page. sorry I wasn't clear with my question earlier.


Answer:

According to the clarifications in comments to the question, you try to position the signature right underneath the bounding box of existing contents of the last document page.

To determine that bounding box you can use the BoundingBoxFinder presented in this answer.

But as you found out in response to a comment to that effect, you cannot simply use its result as input for CreateVisibleSignature.setVisibleSignDesigner as different coordinate systems are assumed:

  • The BoundingBoxFinder uses the PDF default user space coordinates of the page in question: They are given by the MediaBox of the page in question and have their y coordinates increase upwards. Usually the origin is in the lower left corner of the page.
  • CreateVisibleSignature on the other hand uses a coordinate system with the same unit length but having the origin in the upper left corner of the page and the y coordinates increasing downwards.

Thus, the coordinates have to be transformed, e.g. like this:

File documentFile = new File(SOURCE);
File signedDocumentFile = new File(RESULT);

Rectangle2D boundingBox;
PDRectangle mediaBox;
try (   PDDocument document = PDDocument.load(documentFile) ) {
    PDPage pdPage = document.getPage(0);
    BoundingBoxFinder boundingBoxFinder = new BoundingBoxFinder(pdPage);
    boundingBoxFinder.processPage(pdPage);
    boundingBox = boundingBoxFinder.getBoundingBox();
    mediaBox = pdPage.getMediaBox();
}

CreateVisibleSignature signing = new CreateVisibleSignature(ks, PASSWORD.clone());
try (   InputStream imageStream = IMAGE_STREAM) {
    signing.setVisibleSignDesigner(documentFile.getPath(), (int)boundingBox.getX(), (int)(mediaBox.getUpperRightY() - boundingBox.getY()), -50, imageStream, 1);
}
signing.setVisibleSignatureProperties("name", "location", "Security", 0, 1, true);
signing.setExternalSigning(false);
signing.signPDF(documentFile, signedDocumentFile, null);

(CreateSignature test signLikeHemantPdfTest)

Remarks

I found a document looking like your Yukon Education PDF Test File here. Applying the code above to that file, one observes that there is a small gap between the last visible line of text and the image. This gap is caused by some space characters in a line below the "Please visit our website" line. The BoundingBoxFinder does not check whether a drawing instruction eventually results in something visible, it always adds the area in question to the bounding box.

In general you may want to subtract a small bit from the y coordinate calculated by the code above to create a visual gap between former page content and the new signature widget.

Looking into the sources of the CreateVisibleSignature one sees that there actually the y coordinates are transformed by subtracting them from the height of the MediaBox, not from its upper border value. Eventually these coordinates are copied into the target document. Thus, it may be necessary to use mediaBox.getHeight() instead of mediaBox.getUpperRightY() in the code above.

Looking into the sources of the CreateVisibleSignature2 one sees that there actually the CropBox is used instead of the MediaBox. If your code derives from that example, you may have to replace pdPage.getMediaBox() by pdPage.getCropBox() in the code above.

In general this arbitrary use of different coordinate systems is one of the fairly few sources of irritation when working with PDFBox.

Question:

I'm implementing an application to sign PDF files in the server, with the follow scenario (to make long history, short):

  1. Client start signature sending to server, date/time and watermark
  2. Server add signature dictionaries into file and send data to be signed
  3. Client sign content
  4. Server finish the signature

I'm using PDFBox 2.0.15, and making use of new feature saveIncrementalForExternalSigning as shown in the code below:

try {
        String name = document.getID();
        File signedFile = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
        this.log("[SIGNATURE] Creating signed version of the document");
        if (signedFile.exists()) {
            signedFile.delete();
        }
        FileOutputStream tbsFos = new FileOutputStream(signedFile);
        ExternalSigningSupport externalSigning = pdfdoc.saveIncrementalForExternalSigning(tbsFos);

        byte[] content = readExternalSignatureContent(externalSigning);
        if (postparams.get("action").equalsIgnoreCase("calc_hash")) {
            this.log("[SIGNATURE] Calculating hash of the document");
            String strBase64 = ParametersHandle.compressParamBase64(content);

            // this saves the file with a 0 signature
            externalSigning.setSignature(new byte[0]);

            // remember the offset (add 1 because of "<")
            int offset = signature.getByteRange()[1] + 1;

            this.log("[SIGNATURE] Sending calculated hash to APP");
            return new String[] { strBase64, processID, String.valueOf(offset) };
        } else {
            this.log("[SIGNATURE] Signature received from APP");
            String signature64 = postparams.get("sign_disgest");
            byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);

            this.log("[SIGNATURE] Setting signature to document");
            externalSigning.setSignature(cmsSignature);

            pdfdoc.close();

            IOUtils.closeQuietly(signatureOptions);

            this.log("[DOXIS] Creating new version of document on Doxis");
            createNewVersionOfDocument(doxisServer, documentServer, doxisSession, document, signedFile);

            return new String[] { "SIGNOK" };
        }
    } catch (IOException ex) {
        this.log("[SAVE FOR SIGN] " + ex);
        return null;
    }

In the "IF" statement I'm generating data to be signed. In the "ELSE" statement adding the signature, that comes via post request (that is what ParametersHandle.decompressParamFromBase64 does), into document. So I have two post requests for this method in this try.

A second approach was doing each post request in one method, so I have this second code block:

// remember the offset (add 1 because of "<") 
        int offset = Integer.valueOf(postparams.get("offset"));
        this.log("[PDF BOX] Retrieving offset of bytes range for this signature. The value is: "
                + String.valueOf(offset));

        File signedPDF = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
        this.log("[SIGNATURE] Reloading document for apply signature: " + signedPDF.getAbsolutePath());

        // invoke external signature service
        String signature64 = postparams.get("sign_disgest");
        byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);

        this.log("[SIGNATURE] Got signature byte array from APP.");
        // set signature bytes received from the service

        // now write the signature at the correct offset without any PDFBox methods
        this.log("[SIGNATURE] Writing signed document...");
        RandomAccessFile raf = new RandomAccessFile(signedPDF, "rw");
        raf.seek(offset);
        raf.write(Hex.getBytes(cmsSignature));
        raf.close();
        this.log("[SIGNATURE] New signed document has been saved!");

The problem is: I'm getting the error "The document has been altered or corrupted since the Signature was applied" when validating it on Adobe Reader. On my understanding it should not happen since the offset of the signature byte range is being remembered on the second post call.

Any help or idea is appreciated,

Thank you in advance.

[EDIT]

For a complete list of used files: https://drive.google.com/drive/folders/1S9a88lCGaQYujlEyCrhyzqvmWB-68LR3

[EDIT 2]

Based on @mkl comment, here is the method where the signature is made:

public byte[] sign(byte[] hash)
        throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
    PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "");
    X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);

    try
    {
        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        X509Certificate cert = (X509Certificate) certificateChain[0];
        ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
        gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert));
        gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
        CMSProcessableInputStream msg = new CMSProcessableInputStream(new ByteArrayInputStream(hash));
        CMSSignedData signedData = gen.generate(msg, false);
        return signedData.getEncoded();
    }
    catch (GeneralSecurityException e)
    {
        throw new IOException(e);
    }
    catch (CMSException e)
    {
        throw new IOException(e);
    }
    catch (OperatorCreationException e)
    {
        throw new IOException(e);
    }

}

I've tested the CreateVisibleSignature2 examaple, replacing the sign method for one calling this service that returns me the signature, e it works.


Answer:

Thanks to Tilman Hausherr I could figure out what was going on:

1 - I have a Desktop APP that communicates with SmatCards and so on, and it is the signer. To communicate with the server (through a webpage) we use WebSocket. I've written my own websocket server class, and that is why it's only prepared to work with 65k bytes. Than when I tried to send the data here:

ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);           
byte[] cmsSignature = sign(externalSigning.getContent());                           

I got errors in the APP.

2 - Tilman, suggested me to take a look on this @mkl answer where he does the same thing: create a SHA256 hash of the externalSigning.getContent() and send to be signed in another place. I don't know why, but the only thing that didn't work for me was:

gen.addSignerInfoGenerator(builder.build(
                new BcRSAContentSignerBuilder(sha256withRSA,
                        new DefaultDigestAlgorithmIdentifierFinder().find(sha256withRSA))
                                .build(PrivateKeyFactory.createKey(pk.getEncoded())),
                new JcaX509CertificateHolder(cert)));

So, I've replaced this block to:

ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);

Than, my complete signature method is like:

        PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "changeit");
    X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);

        List<X509Certificate> certList = Arrays.asList(certificateChain);
        JcaCertStore certs = new JcaCertStore(certList);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();

        Attribute attr = new Attribute(CMSAttributes.messageDigest,
                new DERSet(new DEROctetString(hash)));

        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(attr);

        SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
                .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v)));

        AlgorithmIdentifier sha256withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA");

        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
        InputStream in = new ByteArrayInputStream(certificateChain[0].getEncoded());
        X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in);

        ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
        gen.addSignerInfoGenerator(builder.build(sha256Signer, new JcaX509CertificateHolder(cert)));

        gen.addCertificates(certs);

        CMSSignedData s = gen.generate(new CMSAbsentContent(), false);
        return s.getEncoded();

So, thank you community once more!!!

Question:

Given a digitally signed PDF file with a signature, I'd like to print this document on paper.

Using PDFBox with the following code I am able to print the document, except for that the signature is not ending up on the thin, bleached sheets of dead tree. The positioning of text around it does not change. So it seems aware of that there should be something there, yet it is not printed.

  import java.awt.print.PrinterJob;
  import javax.print.PrintService;
  import javax.print.attribute.HashPrintRequestAttributeSet;
  import org.apache.pdfbox.pdmodel.PDDocument;
  import org.apache.pdfbox.printing.PDFPageable;

  InputStream pdf = getPDFInputStreamSomeHow();
  PDDocument pdDocument = PDDocument.load(pdf);
  PDFPageable pageable = new PDFPageable(pdDocument);

  PrinterJob job = PrinterJob.getPrinterJob();
  job.setPrintService(service);
  job.setPageable(pageable);
  job.print(attrs);

It seems that PDFBox actually does support this kind of signature, because when I use org.apache.pdfbox.rendering.PDFRenderer to render the page to a BufferedImage, the signature is rendered just as it is in my regular PDF reader (Acrobat or Evince).

How can I get PDFBox to render the signature correctly when printing? I'd rather not mess about with printing BufferedImage's since it would split the document into multiple print jobs and make me responsible for the quality of the rendered image sent to the printer.


Answer:

A workaround for your problem is to use the 4-parameter constructor of PDFPageable with a non-0 value:

public PDFPageable(PDDocument document, Orientation orientation, boolean showPageBorder, float dpi)

setting the 4th parameter to a useful number like 300 results in the image being be rasterized at the given DPI. So for you, the call would be

PDFPageable pageable = new PDFPageable(pdDocument, Orientation.AUTO, false, 300);

A possible cause of printing problems are being tracked in issue PDFBOX-3729. That issue has also another workaround for windows users.

Question:

When using adbe.x509.rsa_sha1 as subfilter in pdfbox, it's required by the specification (32000-1:2008, page 468) to set the 'Cert' signature dictionary field. There is no method in PDSignature or COSDictionary to set this 'Cert' field, that should contain "an array of byte strings that shall represent the x.509 certificate chain (...)".

Is there a way to specify this 'Cert' field anyhow? Or is this not possible for now?


Answer:

As @TimanHausherr mentioned, it's not possible for now to set the cert value directly in PDFBox (2.0.4). Still it is possible to include the 'Cert' entry manually using the following method:

byte[] cert = ...;
PDSignature signature = new PDSignature();
COSString certCosString = new COSString(cert);
signature.getCOSObject().setItem("Cert", certCosString);

The cert field will now be included when PDFBox signs the document with 'signature'.

Question:

I guess my question is rather simple. All I found in my research were threads with very short answers sounding like "DAT SO EZ LUK HEER NAP: link".. I tried those links and they were all 404.. So I'm exposing myself to another public execution and will try this thread a millionth time.

I'm working with PDFBox 2.0.17 and I am trying to sign a PDF-File with an already existing pfx-certificate. Thats pretty much everything. I got some pretty disgusting solutions myself with printing the file via pdf-Creator and stuff, but there must be a smoother, nicer solution.

I'd be pretty thankful for every non-404-Link and will accept any kind of public humiliation.

Best regards, YXCD

P.S.: Of course I found solutions like PDFone and other providers. But I'm trying to do this without getting myself bankrupt ..


Answer:

Okay, to sum this up..

My experience here is that PDFBox has some very precise dependencies which will throw Exceptions the moment they are slightly out of version. I fixed every problem by first of all reloading the entire PDFBox-Files and then downloading every dependencies the exact version as listed in the PDFBox Version. Using newer versions will throw exceptions.

At the end I took the CreateSignature-Example and rewrote it for my needs. Then it all worked perfectly and smoothly.

Thanks to @mkl and @TilmanHausherr for your comments and giving me the right guidelines.

Question:

I'm trying sign a pdf, and my code signed the pdf but, when I open the document in Adobe I have this response: and I don't know what happens.

Certificate generator

public static BigInteger generateSerial() {
        SecureRandom random = new SecureRandom();
        return BigInteger.valueOf(Math.abs(random.nextLong()));
    }

public static X509Certificate CeriticateGenerator(PublicKey publicKey, PrivateKey privateKey)  throws OperatorCreationException, CertificateException, CertIOException {
    Date startDate = new Date(System.currentTimeMillis());
    Date expiryDate = new Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000);

    X500Name issuser=new X500Name("cn=Rubrica");
    X500Name subject=new X500Name("cn=Rubrica");
    X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(issuser,
            generateSerial(),
            startDate,
            expiryDate,
            subject,
            publicKey).addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyId(publicKey))
            .addExtension(Extension.authorityKeyIdentifier, false, createAuthorityKeyId(publicKey))
            .addExtension(Extension.basicConstraints, true, new BasicConstraints(true));

     ContentSigner sigGen = new JcaContentSignerBuilder("SHA512withRSA").setProvider("BC").build(privateKey);
     return new JcaX509CertificateConverter()
          .setProvider(new BouncyCastleProvider()).getCertificate(certGen.build(sigGen));


}
 private static SubjectKeyIdentifier createSubjectKeyId(final PublicKey publicKey) throws OperatorCreationException {
        final SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
        final DigestCalculator digCalc =
          new BcDigestCalculatorProvider().get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));

        return new X509ExtensionUtils(digCalc).createSubjectKeyIdentifier(publicKeyInfo);
      }

      /**
       * Creates the hash value of the authority public key.
       *
       * @param publicKey of the authority certificate
       *
       * @return AuthorityKeyIdentifier hash
       *
       * @throws OperatorCreationException
       */
      private static AuthorityKeyIdentifier createAuthorityKeyId(final PublicKey publicKey)
        throws OperatorCreationException
      {
        final SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
        final DigestCalculator digCalc =
          new BcDigestCalculatorProvider().get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));

        return new X509ExtensionUtils(digCalc).createAuthorityKeyIdentifier(publicKeyInfo);
      }

this is the interface Signature

public class PDBOXsignerManager implements SignatureInterface{
    private PrivateKey privateKey;
    private Certificate[] certificateChain;


    PDBOXsignerManager(KeyStore keyStore, String password, String appCertificateAlias)  {

        try {
            this.certificateChain = Optional.ofNullable(keyStore.getCertificateChain(appCertificateAlias))
                    .orElseThrow(() -> (new IOException("Could not find a proper certificate chain")));
            this.privateKey = (PrivateKey) keyStore.getKey(appCertificateAlias, password.toCharArray());

            Certificate certificate = this.certificateChain[0];

            if (certificate instanceof X509Certificate) {
                ((X509Certificate) certificate).checkValidity();
            }
        } catch (KeyStoreException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (UnrecoverableKeyException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (CertificateExpiredException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (CertificateNotYetValidException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }



    }

    @Override
    public byte[] sign(InputStream content) throws IOException {
        try {
            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
            X509Certificate cert = (X509Certificate) this.certificateChain[0];
            ContentSigner ECDSASigner = new JcaContentSignerBuilder("SHA512withRSA").build(this.privateKey);
            gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(ECDSASigner, cert));
            gen.addCertificates(new JcaCertStore(Arrays.asList(this.certificateChain)));
            CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
            CMSSignedData signedData = gen.generate(msg, false);



            return signedData.getEncoded();
        } catch (GeneralSecurityException | CMSException | OperatorCreationException e) {
            //throw new IOException cause a SignatureInterface, but keep the stacktrace
            throw new IOException(e);
        }
    }
}

this the class signer

public class PDBOXSigner extends PDBOXsignerManager
{
 PDBOXSigner(KeyStore keyStore, String password, String appCertificateAlias) {
    super(keyStore, password, appCertificateAlias);
        }

public void signDetached( PDDocument document, OutputStream output, String name, String reason) {
        PDSignature pdSignature = new PDSignature();
        pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
        pdSignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1);
        pdSignature.setName(name);
        pdSignature.setReason(reason);

        // Se le agrega la fecha de firma necesaria para validar la misma
        pdSignature.setSignDate(Calendar.getInstance());

        // Registro del diccionario de firmas y y la interfaz de firma
        try {
             SignatureOptions signatureOptions = new SignatureOptions();
             // Size can vary, but should be enough for purpose.
             signatureOptions.setPreferredSignatureSize(SignatureOptions.DEFAULT_SIGNATURE_SIZE * 2);
             // register signature dictionary and sign interface
             document.addSignature(pdSignature, this, signatureOptions);

            // write incremental (only for signing purpose)
            document.saveIncremental(output);
            output.flush();
            output.close();

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


    }
}

I created the certificates and key pair with java and bouncycastle, i don't now if is the problem or what am I doing wrong?



Answer:

The mistake was to use SUBFILTER_ADBE_PKCS7_SHA1 instead of the SUBFILTER_ADBE_PKCS7_DETACHED that is used in the official example. SUBFILTER_ADBE_PKCS7_SHA1 shouldn't be used for signing, it is deprecated in PDF 2.0.

Question:


Answer:

try (PDDocument doc = PDDocument.load(new File("....")))
{
    PDPageTree pageTree = doc.getPages();
    PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
    for (PDField field : acroForm.getFieldTree())  // null check omitted
    {
        if (field instanceof PDSignatureField)
        {
            PDSignatureField sigField = (PDSignatureField) field;
            for (PDAnnotationWidget widget : sigField.getWidgets())
            {
                PDPage page = widget.getPage();
                if (page != null)
                {
                    System.out.println("Signature on page " + (pageTree.indexOf(widget.getPage()) + 1));
                }
            }
        }
    }
}