0

I'm trying to add an empty signature field to an existing digitally signed pdf (certify signature).

I have a workflow where many users will sign the document (approval signature), the document is created with "n" empty signature fields, one for each user, our application first apply a invisible certify signature, then each user can sign the document in respective field, but due to changes unexpected in the workflow, other users might want to sign, so we want to add the respective empty signature field and then apply the signature.

I tried to add the empty field (a table with a cell event) to the certified document but when I want to add it and associate the field, it breaks the signature, I cannot make it work correctly.

Here is the methods used to sign,add signature field, and set the signature field options. I don't know what I'm doing wrong.

public static String sign(SignRequest signRequest, File certificate, File unsignedDocument, File image, File icon)
        throws FileNotFoundException, IOException, DocumentException, StreamParsingException, OCSPException,
        OperatorException, URISyntaxException, WriterException, GeneralSecurityException, FontFormatException {

    SignatureType sigType = Optional
            .ofNullable(SignatureType.get(signRequest.getSignatureProperties().getSignatureType()))
            .orElse(SignatureType.APPROVAL_SIGNATURE);

    File signedDocument = File.createTempFile("signed",".pdf");
    char[] pass = signRequest.getKeyStore().getPassword().toCharArray();

    // Load certificate chain
    BouncyCastleProvider provider = new BouncyCastleProvider();
    Security.addProvider(provider);
    KeyStore ks = KeyStore.getInstance("PKCS12", provider.getName());
    ks.load(new FileInputStream(certificate.getAbsolutePath()), pass);

    String alias = getAliasFromKeyStore(ks);
    PrivateKey pk = (PrivateKey) ks.getKey(alias, pass);
    Certificate[] chain = ks.getCertificateChain(alias);

    // Creating the reader and the stamper
    PdfReader reader = new PdfReader(FileUtils.openInputStream(unsignedDocument));
    FileOutputStream os = new FileOutputStream(signedDocument);
    PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0', null, true);
    PdfSignatureAppearance appearance = null;

    // Certify o approval signature (approval is the default signature type)
    switch (sigType) {
    case CERTIFY_SIGNATURE:
        if (reader.getAcroFields().getSignatureNames().size() <= 0) {
            appearance = setSignatureFieldOptions(stamper.getSignatureAppearance(), reader, chain,
                    signRequest, image, icon, Boolean.TRUE);
        } else {
            appearance = setSignatureFieldOptions(stamper.getSignatureAppearance(), reader, chain,
                    signRequest, image, icon, Boolean.FALSE);
        }
        break;
    case APPROVAL_SIGNATURE:
    default:
        appearance = setSignatureFieldOptions(stamper.getSignatureAppearance(), reader, chain, signRequest,
                image, icon, Boolean.FALSE);
        break;
    }

    // Adding LTV (optional)
    OcspClient ocspClient = null;
    List<CrlClient> crlList = null;
    if (signRequest.getSignatureProperties().getLtv() == Boolean.TRUE) {
        ocspClient = new OcspClientBouncyCastle(new OCSPVerifier(null, null));
        CrlClient crlClient = new CrlClientOnline(chain);
        crlList = new ArrayList<CrlClient>();
        crlList.add(crlClient);
    }

    // Adding timestamp (optional)
    TSAClient tsaClient = null;
    if (signRequest.getTimestamp() != null
            && StringUtils.isNotBlank(signRequest.getTimestamp().getUrl())) {
        tsaClient = new TSAClientBouncyCastle(signRequest.getTimestamp().getUrl(),
                signRequest.getTimestamp().getUser(), signRequest.getTimestamp().getPassword());
    }

    // Creating the signature
    ExternalSignature pks = new PrivateKeySignature(pk, signtRequest.getSignatureProperties().getAlgorithm(),
            provider.getName());
    ExternalDigest digest = new BouncyCastleDigest();

    MakeSignature
            .signDetached(appearance, digest, pks, chain, crlList, ocspClient, tsaClient,
                    calculateEstimatedSize(chain, ocspClient, tsaClient, crlList, getEstimatedSizeBonus()), CryptoStandard.CMS);

    return signedDocument.getAbsolutePath();
}


private static PdfSignatureAppearance setSignatureFieldOptions(PdfSignatureAppearance appearance, PdfReader reader, Certificate[] chain, SignRequest signRequest, File image, File icon, Boolean certifySignature) throws MalformedURLException, IOException, DocumentException {
    SignatureProperties sigProperties = signRequest.getSignatureProperties();
    SignatureField sigField = sigProperties.getSignatureField();

    // Creating the appearance
    appearance.setSignatureCreator(Constant.SIGNATURE_CREATOR);
    Optional.ofNullable(sigProperties.getReason()).ifPresent(appearance::setReason);
    Optional.ofNullable(sigProperties.getLocation()).ifPresent(appearance::setLocation);

    if (certifySignature) {
        appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS);
    } else {
        appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
    }


    /**
     * Signature Field Name
     */
    BoundingBox box = sigProperties.getSignatureField().getBoundingBox();
    String fieldName = sigField.getName();
    int pageNumber = sigProperties.getSignatureField().getPage();

    if (!sigField.isVisible()) {
        if (StringUtils.isBlank(sigField.getName())) {
            fieldName = generateFieldName();
            appearance.setVisibleSignature(new Rectangle(0, 0, 0, 0), pageNumber, fieldName);
        } else {
            appearance.setVisibleSignature(new Rectangle(0, 0, 0, 0), pageNumber, fieldName);
        }
    } else {
        Font font = FontFactory.getFont(Optional.ofNullable(sigField.getFontName()).orElse(BaseFont.HELVETICA),
                Optional.ofNullable(sigField.getFontSize()).orElse(6));
        Rectangle rect = null;
        FieldPosition fieldPosition = null;
        
         //ADD EMPTY FIELD
        if (StringUtils.isBlank(sigField.getName()) && box != null) {
            fieldName = generateFieldName();
            rect = new Rectangle(box.getLowerLeftX(), box.getLowerLeftY(), box.getLowerLeftX() + box.getWidth(),
                    box.getLowerLeftY() + box.getHeight());
            appearance.setVisibleSignature(rect, pageNumber, fieldName);

            ////////////////////////////////// TRY TO ADD EXTRA SIGNATURE FIELD///////////////////////////////////////////
            Rectangle documentRectangle = reader.getPageSize(pageNumber);
            PdfStamper stamper = appearance.getStamper();

            float pageMargin = 10;
            float tableMargin = 15;
            int numberOfFields = 1; // 1 sigField

            float headerWidth = (documentRectangle.getWidth() - (pageMargin * 2));

            // Table with signature field
            PdfPTable table = new PdfPTable(1);
            table.setTotalWidth(headerWidth - (tableMargin * 4));
            table.setLockedWidth(Boolean.TRUE);
            table.setWidthPercentage(100);
            float posXTable = (pageMargin + (headerWidth - table.getTotalWidth()) / 2);
            float posYTable = 400; // custom y position
            int height = 70; // custom height

            for (int i = 0; i < numberOfFields; i++) {
                String sigFieldName = String.format(Constant.SIGNATURE_FIELD_PREFIX + "%s", (i + 1));
                table.addCell(createSignatureFieldCell(stamper, sigFieldName, height, pageNumber));
            }
            table.writeSelectedRows(0, -1, posXTable, posYTable, stamper.getOverContent(pageNumber));

            ////////////////////////////////// END TRY TO ADD EXTRA SIGNATURE FIELD///////////////////////////////////////////

        } else {
            //APPLY SIGNATURE TO EXISTING EMPTY FIELD
            List<FieldPosition> acroFields = reader.getAcroFields().getFieldPositions(sigField.getName());

            fieldPosition = acroFields.get(0);
            appearance.setVisibleSignature(fieldName);
        }

        // --------------------------- Custom signature appearance ---------------------
        PdfTemplate t = appearance.getLayer(2);
        Rectangle sigRect = null;

        if (fieldPosition != null) {
            sigRect = fieldPosition.position;
        } else {
            sigRect = new Rectangle(box.getLowerLeftX(), box.getLowerLeftY(), box.getLowerLeftX() + box.getWidth(),
                    box.getLowerLeftY() + box.getHeight());
        }

        // Left rectangle
        Rectangle leftRect = new Rectangle(0, 0, (sigRect.getWidth() / 5), (sigRect.getHeight() / 2));
        ColumnText ct1 = new ColumnText(t);
        ct1.setSimpleColumn(leftRect);

        Image im1 = Image.getInstance(icon.getAbsolutePath());
        float ratio1 = leftRect.getHeight() / im1.getHeight();
        im1.scaleToFit(im1.getWidth() * ratio1, im1.getHeight() * ratio1);

        Paragraph p = createParagraph("Digital sign", font, Constant.PARAGRAPH_LEADING, Constant.MARGIN * 9);

        ct1.addElement(new Chunk(im1, Constant.MARGIN * 10, 0));
        ct1.addElement(p);
        ct1.go();

        // Middle rectangle
        Rectangle middleRect = new Rectangle((sigRect.getWidth() / 5), 0,
                (leftRect.getWidth() + sigRect.getWidth() / 5), (sigRect.getHeight() / 2));
        ColumnText ct2 = new ColumnText(t);
        ct2.setSimpleColumn(middleRect);

        if (visibleSignatureImage != null) {
            Image im2 = Image.getInstance(image.getAbsolutePath());
            float ratio2 = sigRect.getHeight() / im2.getHeight();
            im2.scaleToFit(im2.getWidth() * ratio2, im2.getHeight() * ratio2);
            ct2.addElement(new Chunk(im2, 0, 0));
            ct2.go();
        }

        // TextFields
        List<TextField> textFields = fillSignatureFieldText(chain, sigProperties, font);

        // Right rectangle - Names
        Rectangle rightRectNames = new Rectangle(
                (Constant.MARGIN * 5 + leftRect.getWidth() + middleRect.getWidth()), 0,
                (leftRect.getWidth() + middleRect.getWidth() + sigRect.getWidth() / 4),
                sigRect.getHeight() - Constant.MARGIN);
        ColumnText ct31 = new ColumnText(t);
        ct31.setSimpleColumn(rightRectNames);

        List<Paragraph> paragraphsNames = textFields.stream()
                .map(e -> createParagraph(e.getName(), font, Constant.PARAGRAPH_LEADING, 0))
                .collect(Collectors.toList());
        paragraphsNames.forEach(ct31::addElement);
        ct31.go();

        // Right rectangle - Values
        Rectangle rightRectValues = new Rectangle(
                (Constant.MARGIN * 4 + leftRect.getWidth() + middleRect.getWidth() + rightRectNames.getWidth()), 0,
                sigRect.getWidth(), (sigRect.getHeight() - Constant.MARGIN));
        ColumnText ct32 = new ColumnText(t);
        ct32.setSimpleColumn(rightRectValues);

        List<Paragraph> paragraphsValues = textFields.stream()
                .map(e -> createParagraph(e.getValue(), font, Constant.PARAGRAPH_LEADING, 0))
                .collect(Collectors.toList());
        paragraphsValues.forEach(ct32::addElement);
        ct32.go();
        // --------------------------- Custom signature appearance ---------------------
    }

    return appearance;
}



//this is used to first create the empty fields
protected static PdfPCell createSignatureFieldCell(PdfWriter writer, String name, int height) {
    PdfPCell cell = new PdfPCell();
    cell.setMinimumHeight(height);
    cell.setBackgroundColor(BaseColor.WHITE);
    PdfFormField field = PdfFormField.createSignature(writer);
    field.setFieldName(name);
    field.setFlags(PdfAnnotation.FLAGS_PRINT);
    cell.setCellEvent(new MySignatureFieldEvent(field, null, 0));
    return cell;
}

//this is used to try to add the extra empty field to signed document
protected static PdfPCell createSignatureFieldCell(PdfStamper stamper, String name, int height, int pageNumber) {
    PdfPCell cell = new PdfPCell();
    cell.setMinimumHeight(height);
    cell.setBackgroundColor(BaseColor.WHITE);
    
    
    PdfFormField field = PdfFormField.createSignature(stamper.getWriter());
    field.setFieldName(name);
    field.setFlags(PdfAnnotation.FLAGS_PRINT);
    cell.setCellEvent(new MySignatureFieldEvent(field, stamper, pageNumber));
    return cell;
}


public static class MySignatureFieldEvent implements PdfPCellEvent {

    public PdfFormField field;
    public PdfStamper stamper;
    public int pageField;

    public MySignatureFieldEvent(PdfFormField field, PdfStamper stamper, int pageField) {
        this.field = field;
        this.stamper = stamper;
        this.pageField = pageField;
    }

    public void cellLayout(PdfPCell cell, Rectangle position, PdfContentByte[] canvases) {
        PdfWriter writer = canvases[0].getPdfWriter();
        field.setPage();
        field.setWidget(position, PdfAnnotation.HIGHLIGHT_INVERT);

        if (stamper == null) {
            writer.addAnnotation(field);
        }else {
            stamper.addAnnotation(field, pageField);
        }
    }

}

digital signed document invalid

jhuamanchumo
  • 385
  • 2
  • 7
  • 21
  • Depending on how you read the pdf specification it is not allowed to add new signature fields to certified pdfs. – mkl May 27 '21 at 21:13
  • I have used PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS, so I think that this level allow add signature fields right? but my question would be, how should I red the pdf and add the field without breaking it? – jhuamanchumo May 27 '21 at 21:48

1 Answers1

1

First of all, originally Adobe interpreted the certification levels as described in this answer. In particular, if a document has a certification signature, then adding new fields is forbidden, even signature fields.

This strictness appears to have been lost along the way but may again be applied any time after updates, in particular after the recently published certification attacks on pdf-insecurity.org exploited this relaxed option.

That been said, though, what you never are allowed to do is changing the static page content! In your code, though, you add the additional signature fields by adding a table (with events) to the document. This table will change the static page content.

Thus, try to only add a new signature field.

mkl
  • 90,588
  • 15
  • 125
  • 265
  • adding a table and associating the field was the way to make it visible before signing, I'll try to add only the field. after reading the vulnerability, it means that we only have to use certification P1? from the paper: "For example, level P1 is implemented and any subsequent change is penalized with an invalid certification, while no distinction is made between P2 and P3, and annotations are classified as permitted from P2 onwards. From an attacker’s perspective, this means that for these 11 applications, the attack classes EAA and SSA can be executed at lower permission levels." – jhuamanchumo May 28 '21 at 06:29
  • https://pdf-insecurity.org/download/pdf-certification/paper.pdf – jhuamanchumo May 28 '21 at 06:29
  • *"adding a table and associating the field was the way to make it visible before signing"* - as long as there was no prior signature in the PDF, you could create signature fields any way you wanted, in particular in combination with static table elements. As soon as there is a (filled-in) signature in the PDF, you are limited and must not change the static PDF contents. – mkl May 28 '21 at 06:39
  • *"after reading the vulnerability, it means that we only have to use certification P1?"* - No. Your quote is preceded by "For PDF Architect and Soda PDF we have seen the partial implementation of the permissions." I.e. your quote merely represents the (incorrect!) behavior of those two PDF viewers. You shouldn't try to utilize weaknesses of a few viewers but instead work according to specification. – mkl May 28 '21 at 06:45