2
votes

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.

1
First of all, please reduce your test code to a simple class. There is no need for a Spring application to test pdfbox signing. Furthermore, such frameworks may bring along dependencies which themselves cause issues.mkl
Dear @mkl, I find the problem just now. The Fillable-2.pdf contains 2 signature fields (Signature2 and Signature4) and 2 text fields (Text1 and Text3). Once Signature 2 is signed, Text1 should be locked, and then when Signature4 is signed, Text3 is locked. The job I want to do is, person1 fill something at Text1, and then sign at Signature2, pass to person2, and then person2 fill something at Text3 and sign at Signature4. This process is perfect when I use Acrobat Reader. But when it comes to PDFBox, the 'lock' are disappeared in the output signed PDF!Lawrence Leung
Concerning the locking this answer may help you.mkl
Using the code from that answer I could successfully sign your first signature field.mkl
Dear @mkl, is there any way to lock the corresponding textfield after signing? e.g. Text1Lawrence Leung

1 Answers

3
votes

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)