javapdfpdfboxdigital-signature

How to properly add multiple signatures to a document and sign it externally in a signing session using PDFBox?


I have a requirement to add multiple signatures to a document for a signer in one signing session. Following one of the suggestions from @mkl, I tried to apply his test code [testCreateSignatureWithMultipleVisualizations] (https://github.com/mkl-public/testarea-pdfbox2/blob/master/src/test/java/mkl/testarea/pdfbox2/sign/CreateMultipleVisualizations.java) to my case replacing the loop through document pages by my list of signature metadata (signatureDefinitions). The example code for the 1st try right below:

PDAcroForm acroForm = this.pdDocument.getDocumentCatalog().getAcroForm();
      if (acroForm == null) {
        this.pdDocument.getDocumentCatalog().setAcroForm(acroForm = new PDAcroForm(pdDocument));
      }
      acroForm.setSignaturesExist(true);
      acroForm.setAppendOnly(true);
      acroForm.getCOSObject().setDirect(true);
      PDSignature pdSignature = new PDSignature();
      pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
      pdSignature.setSubFilter(PDSignature.SUBFILTER_ETSI_CADES_DETACHED);
      Calendar signDateCalendar = Calendar.getInstance();
      signDateCalendar.setTime(Date.from(signingTimestamp.toInstant()));
      pdSignature.setSignDate(signDateCalendar);
      pdSignature.setName(userData.getSignatureName());
      pdSignature.setReason(userData.getSignatureReason());
      pdSignature.setLocation(userData.getSignatureLocation());
      pdSignature.setContactInfo(userData.getSignatureContactInfo());
      for (SignatureDefinition signatureDefinition : this.signatureDefinitions) {
        Rectangle2D humanRect =
            new Rectangle2D.Float(
                signatureDefinition.getX(),
                signatureDefinition.getY(),
                signatureDefinition.getWidth(),
                signatureDefinition.getHeight());
        PDRectangle rect =
            SigningPdfDocumentHelper.createSignatureRectangle(
                this.pdDocument, humanRect, signatureDefinition.getPage());

        PDPage pdPage = this.pdDocument.getPage(signatureDefinition.getPage());
        addSignatureField(this.pdDocument, pdPage, rect, pdSignature);
      }
      this.pdDocument.addSignature(pdSignature);
      this.pbSigningSupport = this.pdDocument.saveIncrementalForExternalSigning(this.inMemoryStream);
      MessageDigest digest = MessageDigest.getInstance(digestAlgorithm.getDigestAlgorithm());
      byte[] contentToSign = IOUtils.toByteArray(this.pbSigningSupport.getContent());

      byte[] hashToSign = digest.digest(contentToSign);
      this.base64HashToSign = Base64.getEncoder().encodeToString(hashToSign);
}
  private 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.setValue(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);
    }
  }

Since I did not set the preferred signature size, I got the IOException: "Can't write signature, not enough space". In the second try, I made a change on method this.pdDocument.addSignature(pdSignature) to pass SignatureOptions parameter to have a chance setting preferred signature size like below:

      SignatureOptions options = new SignatureOptions();
    options.setPreferredSignatureSize(30000);
this.pdDocument.addSignature(pdSignature, options);

This time I could sign successfully but only the last signture was displayed, the others remained invisible. signed document here

I would be very grateful if anybody can point the missing part in my implementation to get it completed as all signtures will be visible and valid. Thank you so much for your time reading this topic of mine! Updated: Attach example PDF document signed by my code https://github.com/tai-nguyen-mesoneer/test-data/blob/master/Web%20Application%20Audit%20_Digital%20Onboarding_v1.0-mesoneer%20(003).pdf I am using PDFBox 2.0.24.


Solution

  • In the comments to the question it turned out that the problem here is not as said in the question

    only the last signture was displayed, the others remained invisible

    but that instead only the first signature was invisible and the others were displayed. This issue is analyzed and resolved in this answer.

    Debugging the signing process here

    PDSignature pdSignature = new PDSignature();
    ...
    for (SignatureDefinition signatureDefinition : this.signatureDefinitions) {
        ...
        addSignatureField(this.pdDocument, pdPage, rect, pdSignature);
    }
    this.pdDocument.addSignature(pdSignature);
    

    it turns out that the last line (pdDocument.addSignature(pdSignature)) does not simply add a new signature field with the given signature dictionary but instead first searches the document for an already existing signature field with that signature dictionary, and if it finds one, uses that existing field.

    Thus, while all signature fields created in the for loop were created as visible signatures, the first of them gets hijacked by the addSignature call and changed into an invisible field.

    The simplest way to overcome this is by moving the addSignature call before the for loop:

    PDSignature pdSignature = new PDSignature();
    ...
    this.pdDocument.addSignature(pdSignature);
    for (SignatureDefinition signatureDefinition : this.signatureDefinitions) {
        ...
        addSignatureField(this.pdDocument, pdPage, rect, pdSignature);
    }
    

    Now all the signatures created in the for loop are visible. By the way, this also is how it's being done in my old code you based your solution on.

    The drawback is, though, that there now is an additional, invisible signature created by the addSignature call. The call itself is necessary for PDFBox to know that a signature field is added to be signed, but the field created by that call is not necessary. If you want to get rid of that, you can add a call like this:

    PDSignature pdSignature = new PDSignature();
    ...
    this.pdDocument.addSignature(pdSignature);
    removeSignatureField(acroForm, pdSignature);
    for (SignatureDefinition signatureDefinition : this.signatureDefinitions) {
        ...
        addSignatureField(this.pdDocument, pdPage, rect, pdSignature);
    }
    

    (from CreateMultipleVisualizations test testCreateSignatureWithMultipleVisualizationsWebApplicationAudit_DigitalOnboarding_v10mesoneer003)

    where removeSignatureField is defined like this:

    void removeSignatureField(PDAcroForm acroForm, PDSignature pdSignature) {
        PDSignatureField signatureField = null;
        for (PDField pdField : acroForm.getFieldTree()) {
            if (pdField instanceof PDSignatureField) {
                PDSignature signature = ((PDSignatureField) pdField).getSignature();
                if (signature != null && signature.getCOSObject().equals(pdSignature.getCOSObject())) {
                    signatureField = (PDSignatureField) pdField;
                    break;
                }
            }
        }
    
        if (signatureField != null) {
            COSArray fieldsArray = (COSArray)acroForm.getCOSObject().getDictionaryObject(COSName.FIELDS);
            if (fieldsArray != null)
                fieldsArray.remove(signatureField.getCOSObject());
    
            PDAnnotationWidget widget = signatureField.getWidgets().get(0);
            PDPage page = widget.getPage();
            COSArray annotsArray = (COSArray)page.getCOSObject().getDictionaryObject(COSName.ANNOTS);
            if (annotsArray != null)
                annotsArray.remove(widget.getCOSObject());
        }
    }
    

    (CreateMultipleVisualizations helper method)


    And remember, the warning from my original answer here still holds:

    This kind of multi-visualization signature is not wanted by the PDF specification teams. Chances are that they eventually will be forbidden.