So im currently working on a project to sign PDF documents and I ran into a problem. So i can sign documents correctly without any problems if I specify the block to sign i.e.
signatureAppearance.setVisibleSignature(rectangle, page, signingBlockName);
I can add multiple signatures without a problem and all of the signatures remain valid. Now I have changed the code to first add empty signature blocks and then sign these blocks using the signature block name that i've added i.e.
signatureAppearance.setVisibleSignature(signingBlockName);
The reason for this is that we are going to generate PDF documents with signature fields already in place(there can be multiple) but the problem here is that since i've started using this method of signing the signature blocks, the second signature invalidates the first one even though the signing is done in append mode, and only the last signature has the PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED certification level.
Am i missing something or is there a bug in the older version of IText? Im currently using IText version 4.2.0.
Thanks in advance.
------Code-------------
/**
* The method is used to sign a pdf document using a certificate, image of the signature as well as specified signature blocks
*
* @param document the document to be signed
* @param certificate the certificate to be used to do the signing
* @param signature the image of the signature used to sign the document
* @param signatureBlocks the blocks in the pdf document to sign
* @return the signed document bytes
* @throws Exception
*/
public byte[] signPDFDocument(PDFDocument document, PKSCertificate certificate, SignatureImage signature, List<SigningBlock> signatureBlocks, boolean certifyDocument) throws Exception
{
document.addSignatureBlocks(signatureBlocks);
PDFDocument signedDocument = signPDFDocumentSignatureBlocks(document, certificate, signature, signatureBlocks, certifyDocument);
return signedDocument.getDocumentBytes();
}
/**
* The method is used to sign a pdf document using a certificate, image of the signature as well as specified signature blocks
*
* @param document the document to be signed
* @param certificate the certificate to be used to do the signing
* @param signatureBlocks the blocks in the pdf document to sign
* @return the signed document bytes
* @throws Exception
*/
public byte[] signPDFDocument(PDFDocument document, PKSCertificate certificate, List<SigningBlock> signatureBlocks, boolean certifyDocument) throws Exception
{
document.addSignatureBlocks(signatureBlocks);
PDFDocument signedDocument = signPDFDocumentSignatureBlocks(document, certificate, null, signatureBlocks, certifyDocument);
return signedDocument.getDocumentBytes();
}
/**
* The method is used to get the names of all signature fields
*
* @param document the document to check for the signature fields
* @return the list of signature field names used and unused
* @throws Exception
*/
public List<String> getAvailableSignatureBlocks(PDFDocument document) throws Exception
{
PdfReader reader = new PdfReader(document.getDocumentBytes());
ArrayList arrayList = reader.getAcroFields().getBlankSignatureNames();
return Arrays.asList((String[]) arrayList.toArray(new String[arrayList.size()]));
}
/**
* This method is used to loop over the signature blocks and sign them
*
* @param document the document to be signed
* @param certificate the certificate to apply to the signature
* @param signature the image of the client signature
* @param signatureBlocks the signature blocks to create and sign
* @param certifyDocument flag to indicate if the document should be signed
* @throws Exception
*/
private PDFDocument signPDFDocumentSignatureBlocks(PDFDocument document, PKSCertificate certificate, SignatureImage signature, List<SigningBlock> signatureBlocks, boolean certifyDocument) throws Exception
{
for (int i = 0; i < signatureBlocks.size();i++)
{
PDFDocument signedDocument = new PDFDocument(new ByteArrayOutputStream());
PdfReader reader = new PdfReader(document.getDocumentBytes());
PdfStamper stamper = createPDFStamper(signedDocument, reader);
document = signPDFDocumentSignatureBlock(signedDocument, certificate, signature, signatureBlocks.get(i), stamper, certifyDocument && i == (signatureBlocks.size() - 1));
}
return document;
}
/**
* The method is used to sign the pdf document, it also marks the signing process if it is the final signature process
*
* @param signedDocument the signed document to be generated
* @param certificate the certificate to be used to do the signing
* @param signature the image of the signature used to sign the document
* @param signingBlock the current block to sign in the document
* @param stamper the current document stamper reference
* @param certifyDocument indicate if this signing should certify the document
* @return the signed document object
* @throws Exception
*/
private PDFDocument signPDFDocumentSignatureBlock(PDFDocument signedDocument, PKSCertificate certificate, SignatureImage signature, SigningBlock signingBlock, PdfStamper stamper, boolean certifyDocument) throws Exception
{
PdfSignatureAppearance appearance = signWithSignatureImage(stamper, signature, signingBlock.getName(), certifyDocument);
signWithCertificate(certificate, appearance);
signWithTimeStampingAuthority(appearance, certificate);
signedDocument.updateBytesFromByteStream();
return signedDocument;
}
/**
* The method is used to get the instance of the PDF stamper to stamp the document
*
* @param signedDocument the document that is currently being signed
* @param reader the reader that is reading the document to sign.
* @return the stamper instance
* @throws Exception
*/
private PdfStamper createPDFStamper(PDFDocument signedDocument, PdfReader reader) throws Exception
{
return PdfStamper.createSignature(reader, signedDocument.getByteStream(), '\0', null, true);
}
/**
* The method is used to add the signature image to the signing block
*
* @param stamper the current pdf stamping reference
* @param signature the image to apply to the stamper
* @param signingBlockName the block to sign
* @param certifyDocument indicate if this signing should certify the document
* @throws Exception
*/
private PdfSignatureAppearance signWithSignatureImage(PdfStamper stamper, SignatureImage signature, String signingBlockName, boolean certifyDocument) throws Exception
{
PdfSignatureAppearance signatureAppearance = stamper.getSignatureAppearance();
signatureAppearance.setVisibleSignature(signingBlockName);
setImageForSignature(signatureAppearance, signature);
certifyDocumentSignature(signatureAppearance, certifyDocument);
return signatureAppearance;
}
/**
* The method is used to add an image to the signature block
*
* @param signatureAppearance the reference to the current document appearance
* @param signature the image to apply to the signature
* @throws Exception
*/
private void setImageForSignature(PdfSignatureAppearance signatureAppearance, SignatureImage signature) throws Exception
{
if(signature != null)
{
signatureAppearance.setImage(Image.getInstance(signature.getSignatureImage(), null));
}
}
/**
* The method is used to mark the signature as the certification signature
*
* @param signatureAppearance the reference to the current document appearance
* @param certifyDocument indicates if the document should be certified
*/
private void certifyDocumentSignature(PdfSignatureAppearance signatureAppearance, boolean certifyDocument)
{
if(certifyDocument)
{
signatureAppearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
}
}
/**
* The method is used to add the text containing information about the certificate to the signing appearance
*
* @param certificate the certificate to be used to do the signing
* @param signatureAppearance the appearance of the signature on the document
* @throws Exception
*/
private void signWithCertificate(PKSCertificate certificate, PdfSignatureAppearance signatureAppearance) throws Exception
{
signatureAppearance.setLayer2Text(buildTextSignature(certificate));
signatureAppearance.setLayer2Font(new Font(Font.COURIER, 9));
signatureAppearance.setAcro6Layers(true);
signatureAppearance.setRender(PdfSignatureAppearance.SignatureRenderDescription);
}
/**
* The method is used to encrypt the document using the certificate as well as a timestamping authority
*
* @param appearance the appearance of the signature on the document
* @param certificate the certificate to be used to do the signing
* @throws Exception
*/
private void signWithTimeStampingAuthority(PdfSignatureAppearance appearance, PKSCertificate certificate) throws Exception
{
_timestampingService.signWithTimestampingAuthority(appearance, certificate);
}
/**
* The method builds the text that is used in the text representation of the signature
*
* @param certificate the certificate to be used to do the signing
* @return the text representation of certificate information
* @throws Exception
*/
private String buildTextSignature(PKSCertificate certificate) throws Exception
{
String organization = certificate.getCertificateFieldByName("O");
String commonName = certificate.getCertificateFieldByName("CN");
String signDate = new SimpleDateFormat(_datetimeFormat).format(Calendar.getInstance().getTime());
String expirationDate = new SimpleDateFormat(_datetimeFormat).format(((X509Certificate)certificate.getCertificateChain()[0]).getNotAfter());
return "Digitally signed by " + organization + "\nSignee: " + commonName + "\nSign date: " + signDate + "\n" + "Expiration date: " + expirationDate;
}
As already conjectured in an initial comment, this actually turned out to be the problem: Only the first signature of a document may be a certification signature. Later signature may only (PDF 2.0) restrict the access permissions using a signature field lock entry, cf. the specification:
In the upcoming version 2.0 of the PDF standard (ISO-32000-2) it will be possible to restrict access permissions using a signature field lock entry:
This already is supported by Adobe Reader/Acrobat and also by iText (since some 5.3'ish version IIRC) and seems to allow the effect the OP is after, i.e. disallowing any changes after the final signature field.