Adding new signature by reatining existing unsigned signature fields using PDFBox

236 views Asked by At

I want to sign PDF which has signature fields in it already. I need to add new signature field retaining existing unsigned signature fields. After signing such PDFs, I see that new signature field added by code is always invalid. Says 'document has been altered'.

Below code being used to compute hash of the document:

private DocumentSignatureStructure createSignatureStructureAndComputeHash(byte[] inputFile, File tempFile,
                                                                              SignatureProperties sigProperties)
            throws IOException, NoSuchAlgorithmException {

        try (FileOutputStream fos = new FileOutputStream(tempFile);
             PDDocument doc = PDDocument.load(inputFile);
             SignatureOptions signatureOptions = new SignatureOptions();) {

            signatureOptions.setPreferredSignatureSize(SignatureOptions.DEFAULT_SIGNATURE_SIZE * 2);
            signatureOptions.setPage(sigProperties.getPage() - 1);
            if (sigProperties.isVisibleSignature()) {
                PDRectangle rect = createSignatureRectangle(doc, sigProperties);
                signatureOptions.setVisualSignature(createVisualSignatureTemplate(doc, rect, sigProperties));
            }


            PDSignature signature = new PDSignature();
            signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
            signature.setSignDate(Calendar.getInstance());
            doc.addSignature(signature, signatureOptions);
            ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);

            MessageDigest digest = MessageDigest.getInstance(sigProperties.getHashAlgorithm().getAlgoName());
            byte[] hashBytes = digest.digest(IOUtils.toByteArray(externalSigning.getContent()));
            String base64Hash = Base64.toBase64String(hashBytes);
            externalSigning.setSignature(new byte[0]);
            int offset = signature.getByteRange()[1] + 1;
            IOUtils.closeQuietly(signatureOptions);
            return DocumentSignatureStructure.builder().offset(offset)
                    .hashValue(base64Hash)
                    .build();
        }
    }

Embedding signature code:

byte[] originalDocumentByte = docBlob.getBytes(1L, (int) docBlob.length());
            File file = new File(getTempFolderPath(), getTempFileName("signed"));
            try (FileOutputStream fos = new FileOutputStream(file);) {
                fos.write(originalDocumentByte);
            }
            try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
                raf.seek(documentSignatureStructure.getOffset());
                raf.write(Hex.getBytes(Base64.decode(encodedSignature)));
            }
            Blob signedAndLtvBlob;
            try (PDDocument doc = PDDocument.load(file);
                 FileOutputStream fos = new FileOutputStream(file);
                 FileInputStream fis = new FileInputStream(file)) {
                if (createDss) {
                    log.info("Adding revocation information to DSS dictionary of PDF");
                    makeLtv(doc, revocationData);
                }
                doc.saveIncremental(fos);
            }

It did not work with above code.

Googled up and saw few solutions where COSObject 'NeedToBeUpdated' flag needs to be set to true. Added below code block before adding new signature fields in above code.

//..
if (sigProperties.isVisibleSignature()) {
                PDRectangle rect = createSignatureRectangle(doc, sigProperties);
                signatureOptions.setVisualSignature(createVisualSignatureTemplate(doc, rect, sigProperties));
            }

            PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
            COSDictionary catalogDictionary = doc.getDocumentCatalog().getCOSObject();
            catalogDictionary.setNeedToBeUpdated(true);
            COSDictionary acroFormDictionary = (COSDictionary) catalogDictionary.getDictionaryObject(COSName.ACRO_FORM);
            acroFormDictionary.setNeedToBeUpdated(true);
            COSArray array = (COSArray) acroFormDictionary.getDictionaryObject(COSName.FIELDS);
            array.setNeedToBeUpdated(true);
            for (PDField field : acroForm.getFieldTree()) {
                if (field instanceof PDSignatureField) {
                    COSDictionary fieldDictionary = field.getCOSObject();
                    COSDictionary dictionary = (COSDictionary) fieldDictionary.getDictionaryObject(COSName.AP);
                    dictionary.setNeedToBeUpdated(true);
                    COSStream stream = (COSStream) dictionary.getDictionaryObject(COSName.N);
                    stream.setNeedToBeUpdated(true);
                    while (fieldDictionary != null)
                    {
                        fieldDictionary.setNeedToBeUpdated(true);
                        fieldDictionary = (COSDictionary) fieldDictionary.getDictionaryObject(COSName.PARENT);
                    }
                }
            }
            
            PDSignature signature = new PDSignature();
            signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
//..

Even this did not work.

Resulting PDF shows signature is invalid: enter image description here

PDF used for signing with signature fields: enter image description here

Whats the piece I am missing here?

PDF file : https://drive.google.com/file/d/1-vu9_WIfFo198v6AxoBMxCuyX1rE2FOS/view?usp=share_link

Signed PDF (invalid): https://drive.google.com/file/d/1DD0aKVkonH9a_CfGrj9mACe6DBt4Ijsj/view?usp=share_link

2

There are 2 answers

0
mkl On

Something indeed damaged your file, and it's apparently not the code you show.

Comparing your signed file "Formulier DSS-01 - DC+QV Onboarding Checklist-signed.pdf" with your original "Formulier DSS-01 - DC+QV Onboarding Checklist.pdf", one sees that the former is not simply the latter with incremental updates as one would expect but that some regions in that file part have been overwritten with zeros.

The regions in question are from 0x2c000-0x2cfff and 0x40000-0x40fff.

After fixing this issue by copying the contents from those regions of the original file into the signed file, Adobe Acrobat validates the signature positively.

So you should try and find out what zeroed those two 4KB regions in your file.

0
Rajath R Joshi On

I was looking for mistake in code for generating hash of the document. But the issue actually was with the way file streams were used to write CMSSignedData and adding DSS dictionary for making signing PDF LTV.

Using same File for loading and writing was causing some parts of file getting overwritten by zeros.

Below piece of code for embedding external signature solved my issue.

Blob docBlob = documentSignatureStructure.getReadyDocument();                      
byte[] originalDocumentByte = docBlob.getBytes(1L, (int) docBlob.length());        
File signedFile = new File(getTempFolderPath(), getTempFileName("signed_"));       
try (FileOutputStream fos = new FileOutputStream(signedFile)) {                    
    fos.write(originalDocumentByte);                                               
}                                                                                  
try (RandomAccessFile raf = new RandomAccessFile(signedFile, "rw")) {              
    raf.seek(documentSignatureStructure.getOffset());                              
    raf.write(Hex.getBytes(Base64.decode(encodedSignature)));                      
}                                                                                  
                                                                                   
File signedLtvFile = new File(getTempFolderPath(), getTempFileName("signedLtv_")); 
try (PDDocument doc = PDDocument.load(signedFile);                                 
     FileOutputStream fos = new FileOutputStream(signedLtvFile)) {                 
    if (createDss) {                                                               
        log.info("Adding revocation information to DSS dictionary of PDF");        
        makeLtv(doc, revocationData);                                              
        doc.saveIncremental(fos);                                                  
    }                                                                              
}                                                                                  
                                                                                   
Blob signedAndLtvBlob;                                                             
try (FileInputStream fis = new FileInputStream(signedLtvFile)) {                   
    signedAndLtvBlob = new javax.sql.rowset.serial.SerialBlob(fis.readAllBytes()); 
}                                                                                  
                                                                                   
Files.delete(signedFile.toPath());                                                 
log.debug("temp signed file {} deleted successfully", signedFile.getName());       
Files.delete(signedLtvFile.toPath());                                              
log.debug("temp ltv signed file {} deleted successfully", signedLtvFile.getName());
return signedAndLtvBlob;