How to use pdfbox to digitally sign dynamically created PDF documents?
I'm sorry! I'm poor in Java Please correct me where I am wrong and improve where I am poor!
I'm trying to use PDF@R_778_2419 @Digitally sign dynamically created PDFs using the following procedure:
The tasks of the plan: (I) create a template PDF (II) update byterange, XRef, startxref (III) create and build an original file for the signature (IV) create a separate envelope digital signature (V) build a digitally signed PDF document by connecting the original document part – I, independent signature and original PDF part – II
Observation: (I) pdffileoutputstream write(documentOutputStream.toByteArray()); Create a template PDF document using visible signature
(II) it creates some PDF signed documents with errors (a) invalid tokens and (b) several parser errors (now corrected under the guidance of MKL's capabilities!)
Please suggest the following:
(i) How to add signature text to visible signature on layer2
Thank you in advance!
package digitalsignature; import java.awt.geom.AffineTransform; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.security.Signature; import java.util.ArrayList; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSTypedData; import org.bouncycastle.cms.SignerInfoGenerator; import org.bouncycastle.cms.SignerInfoGeneratorBuilder; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; import org.bouncycastle.util.Store; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.CertStore; import java.security.cert.Certificate; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.X509Certificate; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.pdf@R_778_2419@.cos.COSArray; import org.apache.pdf@R_778_2419@.cos.COSDictionary; import org.apache.pdf@R_778_2419@.cos.COSName; import org.apache.pdf@R_778_2419@.pdmodel.PDDocument; import org.apache.pdf@R_778_2419@.pdmodel.PDPage; import org.apache.pdf@R_778_2419@.pdmodel.PDResources; import org.apache.pdf@R_778_2419@.pdmodel.common.PDRectangle; import org.apache.pdf@R_778_2419@.pdmodel.common.PDStream; import org.apache.pdf@R_778_2419@.pdmodel.edit.PDPageContentStream; import org.apache.pdf@R_778_2419@.pdmodel.font.PDFont; import org.apache.pdf@R_778_2419@.pdmodel.font.PDType1Font; import org.apache.pdf@R_778_2419@.pdmodel.graphics.xobject.PDJpeg; import org.apache.pdf@R_778_2419@.pdmodel.graphics.xobject.PDXObjectForm; import org.apache.pdf@R_778_2419@.pdmodel.interactive.annotation.PDAppearanceDictionary; import org.apache.pdf@R_778_2419@.pdmodel.interactive.annotation.PDAppearanceStream; import org.apache.pdf@R_778_2419@.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdf@R_778_2419@.pdmodel.interactive.digitalsignature.SignatureOptions; import org.apache.pdf@R_778_2419@.pdmodel.interactive.form.PDAcroForm; import org.apache.pdf@R_778_2419@.pdmodel.interactive.form.PDField; import org.apache.pdf@R_778_2419@.pdmodel.interactive.form.PDSignatureField; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.CMSSignedDataGenerator; import org.bouncycastle.cms.CMSSignedGenerator; import org.bouncycastle.jce.provider.BouncyCastleProvider; public class AffixSignature { String path = "D:\\reports\\"; String onlyFileName = ""; String pdfExtension = ".pdf"; String pdfFileName = ""; String pdfFilePath = ""; String signedPdfFileName = ""; String signedPdfFilePath = ""; String ownerPassword = ""; String tempSignedPdfFileName = ""; String tempSignedPdfFilePath = ""; String userPassword = ""; String storePath = "resources/my.p12"; String entryAlias = "signerCert"; String keyStorePassword = "password"; ByteArrayOutputStream documentOutputStream = null; private Certificate[] certChain; private static BouncyCastleProvider BC = new BouncyCastleProvider(); int offsetContentStart = 0; int offsetContentEnd = 0; int secondPartLength = 0; int offsetStartxrefs = 0; String contentString = ""; OutputStream signedPdfFileOutputStream; OutputStream pdfFileOutputStream; public AffixSignature() { try { SimpleDateFormat timeFormat = new SimpleDateFormat("hh_mm_ss"); onlyFileName = "Report_" + timeFormat.format(new Date()); pdfFileName = onlyFileName + ".pdf"; pdfFilePath = path + pdfFileName; File pdfFile = new File(pdfFilePath); pdfFileOutputStream = new FileOutputStream(pdfFile); signedPdfFileName = "Signed_" + onlyFileName + ".pdf"; signedPdfFilePath = path + signedPdfFileName; File signedPdfFile = new File(signedPdfFilePath); signedPdfFileOutputStream = new FileOutputStream(signedPdfFile); String tempFileName = "Temp_Report_" + timeFormat.format(new Date()); String tempPdfFileName = tempFileName + ".pdf"; String tempPdfFilePath = path + tempPdfFileName; File tempPdfFile = new File(tempPdfFilePath); OutputStream tempSignedPdfFileOutputStream = new FileOutputStream(tempPdfFile); PDDocument document = new PDDocument(); PDDocumentCatalog catalog = document.getDocumentCatalog(); PDPage page = new PDPage(PDPage.PAGE_SIZE_A4); PDPageContentStream contentStream = new PDPageContentStream(document,page); PDFont font = PDType1Font.HELVETICA; Map<String,PDFont> fonts = new HashMap<String,PDFont>(); fonts = new HashMap<String,PDFont>(); fonts.put("F1",font); // contentStream.setFont(font,12); contentStream.setFont(font,12); contentStream.beginText(); contentStream.moveTextPositionByAmount(100,700); contentStream.drawString("DIGITAL SIGNATURE TEST"); contentStream.endText(); contentStream.close(); document.addPage(page); //To Affix Visible Digital Signature PDAcroForm acroForm = new PDAcroForm(document); catalog.setAcroForm(acroForm); PDSignatureField sf = new PDSignatureField(acroForm); PDSignature pdSignature = new PDSignature(); page.getAnnotations().add(sf.getWidget()); pdSignature.setName("sign"); pdSignature.setByteRange(new int[]{0,0}); pdSignature.setContents(new byte[4 * 1024]); pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); pdSignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); pdSignature.setName("NAME"); pdSignature.setLocation("LOCATION"); pdSignature.setReason("Security"); pdSignature.setSignDate(Calendar.getInstance()); List<PDField> acroFormFields = acroForm.getFields(); sf.setSignature(pdSignature); sf.getWidget().setPage(page); COSDictionary acroFormDict = acroForm.getDictionary(); acroFormDict.setDirect(true); acroFormDict.setInt(COSName.SIG_FLAGS,3); acroFormFields.add(sf); PDRectangle frmRect = new PDRectangle(); // float[] frmRectParams = {lowerLeftX,lowerLeftY,upperRightX,upperRight}; // float[] frmRectLowerLeftUpperRightCoordinates = {5f,page.getMedia@R_778_2419@().getHeight() - 50f,100f,page.getMedia@R_778_2419@().getHeight() - 5f}; float[] frmRectLowerLeftUpperRightCoordinates = {5f,5f,205f,55f}; frmRect.setUpperRightX(frmRectLowerLeftUpperRightCoordinates[2]); frmRect.setUpperRightY(frmRectLowerLeftUpperRightCoordinates[3]); frmRect.setLowerLeftX(frmRectLowerLeftUpperRightCoordinates[0]); frmRect.setLowerLeftY(frmRectLowerLeftUpperRightCoordinates[1]); sf.getWidget().setRectangle(frmRect); COSArray procSetArr = new COSArray(); procSetArr.add(COSName.getPDFName("PDF")); procSetArr.add(COSName.getPDFName("Text")); procSetArr.add(COSName.getPDFName("ImageB")); procSetArr.add(COSName.getPDFName("ImageC")); procSetArr.add(COSName.getPDFName("ImageI")); String signImageFilePath = "resources/sign.JPG"; File signImageFile = new File(signImageFilePath); InputStream signImageStream = new FileInputStream(signImageFile); PDJpeg img = new PDJpeg(document,signImageStream); PDResources holderFormResources = new PDResources(); PDStream holderFormStream = new PDStream(document); PDXObjectForm holderForm = new PDXObjectForm(holderFormStream); holderForm.setResources(holderFormResources); holderForm.setB@R_778_2419@(frmRect); holderForm.setFormType(1); PDAppearanceDictionary appearance = new PDAppearanceDictionary(); appearance.getCOSObject().setDirect(true); PDAppearanceStream appearanceStream = new PDAppearanceStream(holderForm.getCOSStream()); appearance.setNormalAppearance(appearanceStream); sf.getWidget().setAppearance(appearance); acroFormDict.setItem(COSName.DR,holderFormResources.getCOSDictionary()); PDResources innerFormResources = new PDResources(); PDStream innerFormStream = new PDStream(document); PDXObjectForm innerForm = new PDXObjectForm(innerFormStream); innerForm.setResources(innerFormResources); innerForm.setB@R_778_2419@(frmRect); innerForm.setFormType(1); String innerFormName = holderFormResources.addXObject(innerForm,"FRM"); PDResources imageFormResources = new PDResources(); PDStream imageFormStream = new PDStream(document); PDXObjectForm imageForm = new PDXObjectForm(imageFormStream); imageForm.setResources(imageFormResources); byte[] AffineTransformParams = {1,1,0}; AffineTransform affineTransform = new AffineTransform(AffineTransformParams[0],AffineTransformParams[1],AffineTransformParams[2],AffineTransformParams[3],AffineTransformParams[4],AffineTransformParams[5]); imageForm.setMatrix(affineTransform); imageForm.setB@R_778_2419@(frmRect); imageForm.setFormType(1); String imageFormName = innerFormResources.addXObject(imageForm,"n"); String imageName = imageFormResources.addXObject(img,"img"); innerForm.getResources().getCOSDictionary().setItem(COSName.PROC_SET,procSetArr); page.getCOSDictionary().setItem(COSName.PROC_SET,procSetArr); innerFormResources.getCOSDictionary().setItem(COSName.PROC_SET,procSetArr); imageFormResources.getCOSDictionary().setItem(COSName.PROC_SET,procSetArr); holderFormResources.getCOSDictionary().setItem(COSName.PROC_SET,procSetArr); String holderFormComment = "q 1 0 0 1 0 0 cm /" + innerFormName + " Do Q \n"; String innerFormComment = "q 1 0 0 1 0 0 cm /" + imageFormName + " Do Q\n"; String imgFormComment = "q " + 100 + " 0 0 50 0 0 cm /" + imageName + " Do Q\n"; appendRawCommands(holderFormStream.createOutputStream(),holderFormComment); appendRawCommands(innerFormStream.createOutputStream(),innerFormComment); appendRawCommands(imageFormStream.createOutputStream(),imgFormComment); documentOutputStream = new ByteArrayOutputStream(); document.save(documentOutputStream); document.close(); tempSignedPdfFileOutputStream.write(documentOutputStream.toByteArray()); generateSignedPdf(); } catch (Exception e) { e.printStackTrace(); } } public void appendRawCommands(OutputStream os,String commands) throws IOException { os.write(commands.getBytes("ISO-8859-1")); os.close(); } public void generateSignedPdf() { try { //Find the Initial Byte Range Offsets String docString = new String(documentOutputStream.toByteArray(),"ISO-8859-1"); offsetContentStart = (documentOutputStream.toString().indexOf("Contents <") + 10 - 1); offsetContentEnd = (documentOutputStream.toString().indexOf("000000>") + 7); secondPartLength = (documentOutputStream.size() - documentOutputStream.toString().indexOf("000000>") - 7); //Calculate the Updated ByteRange String initByteRange = ""; if (docString.indexOf("/ByteRange [0 1000000000 1000000000 1000000000]") > 0) { initByteRange = "/ByteRange [0 1000000000 1000000000 1000000000]"; } else if (docString.indexOf("/ByteRange [0 0 0 0]") > 0) { initByteRange = "/ByteRange [0 0 0 0]"; } else { System.out.println("No /ByteRange Token is Found!"); System.exit(1); } String interimByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]"; int byteRangeLengthDifference = interimByteRange.length() - initByteRange.length(); offsetContentStart = offsetContentStart + byteRangeLengthDifference; offsetContentEnd = offsetContentEnd + byteRangeLengthDifference; String finalByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]"; byteRangeLengthDifference += interimByteRange.length() - finalByteRange.length(); //Replace the ByteRange docString = docString.replace(initByteRange,finalByteRange); //Update xref Table int xrefOffset = docString.indexOf("xref"); int startObjOffset = docString.indexOf("0000000000 65535 f") + "0000000000 65535 f".length() + 1; int trailerOffset = docString.indexOf("trailer") - 2; String initialXrefTable = docString.substring(startObjOffset,trailerOffset); int signObjectOffset = docString.indexOf("/Type /Sig") - 3; String updatedXrefTable = ""; while (initialXrefTable.indexOf("n") > 0) { String currObjectRefEntry = initialXrefTable.substring(0,initialXrefTable.indexOf("n") + 1); String currObjectRef = currObjectRefEntry.substring(0,currObjectRefEntry.indexOf(" 00000 n")); int currObjectOffset = Integer.parseInt(currObjectRef.trim().replaceFirst("^0+(?!$)","")); if ((currObjectOffset + byteRangeLengthDifference) > signObjectOffset) { currObjectOffset += byteRangeLengthDifference; int currObjectOffsetDigitsCount = Integer.toString(currObjectOffset).length(); currObjectRefEntry = currObjectRefEntry.replace(currObjectRefEntry.substring(currObjectRef.length() - currObjectOffsetDigitsCount,currObjectRef.length()),Integer.toString(currObjectOffset)); updatedXrefTable += currObjectRefEntry; } else { updatedXrefTable += currObjectRefEntry; } initialXrefTable = initialXrefTable.substring(initialXrefTable.indexOf("n") + 1); } //Replace with Updated xref Table docString = docString.substring(0,startObjOffset).concat(updatedXrefTable).concat(docString.substring(trailerOffset)); //Update startxref int startxrefOffset = docString.indexOf("startxref"); //Replace with Updated startxref docString = docString.substring(0,startxrefOffset).concat("startxref\n".concat(Integer.toString(xrefOffset))).concat("\n%%EOF\n"); //Construct Original Document For Signature by Removing Temporary Enveloped Detached Signed Content(000...000) contentString = docString.substring(offsetContentStart + 1,offsetContentEnd - 1); String docFirstPart = docString.substring(0,offsetContentStart); String docSecondPart = docString.substring(offsetContentEnd); String docForSign = docFirstPart.concat(docSecondPart); //Generate Signature pdfFileOutputStream.write(docForSign.getBytes("ISO-8859-1")); File keyStorefile = new File(storePath); InputStream keyStoreInputStream = new FileInputStream(keyStorefile); KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(keyStoreInputStream,keyStorePassword.tocharArray()); certChain = keyStore.getCertificateChain(entryAlias); PrivateKey privateKey = (PrivateKey) keyStore.getKey(entryAlias,keyStorePassword.tocharArray()); List<Certificate> certList = new ArrayList<Certificate>(); certList = Arrays.asList(certChain); Store store = new JcaCertStore(certList); // String algorithm="SHA1WithRSA"; // String algorithm="SHA2WithRSA"; String algorithm = "MD5WithRSA"; //String algorithm = "DSA"; //Updated Sign Method CMSTypedData msg = new CMSProcessableByteArray(docForSign.getBytes("ISO-8859-1")); CMSSignedDataGenerator generator = new CMSSignedDataGenerator(); /* Build the SignerInfo generator builder,that will build the generator... that will generate the SignerInformation... */ SignerInfoGeneratorBuilder signerInfoBuilder = new SignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider(BC).build()); //JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder("SHA2withRSA"); JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder(algorithm); contentSigner.setProvider(BC); SignerInfoGenerator signerInfoGenerator = signerInfoBuilder.build(contentSigner.build(privateKey),new X509CertificateHolder(certList.get(0).getEncoded())); generator.addSignerInfoGenerator(signerInfoGenerator); generator.addCertificates(store); CMSSignedData signedData = generator.generate(msg,false); String apHexEnvelopedData = org.apache.commons.codec.binary.Hex.encodeHexString(signedData.getEncoded()).toUpperCase(); //Construct Content Tag Data contentString = apHexEnvelopedData.concat(contentString).substring(0,contentString.length()); contentString = "<".concat(contentString).concat(">"); //Construct Signed Document String signedDoc = docFirstPart.concat(contentString).concat(docSecondPart); //Write Signed Document to File signedPdfFileOutputStream.write(signedDoc.getBytes("ISO-8859-1")); signedPdfFileOutputStream.close(); signedDoc = null; } catch (Exception e) { throw new RuntimeException("Error While Generating Signed Data",e); } } public static void main(String[] args) { AffixSignature affixSignature = new AffixSignature(); } }
Under the guidance of MKL's ability, the updated code now signs the newly created document Thank you MKL!
Solution
Although these tips were originally presented as comments on the original question, they are now worth formulating as answers:
Code problem
Although too much code needs to be reviewed and repaired without spending a lot of time, and although the initial lack of sample PDF is an obstacle, a quick scan of the code will find some problems:
>The appendrawcommands (xxxforms stream. Createoutputstream(), YYY) call is likely to cause PDF@R_778_2419 @There is a problem: creating the output stream of the same form multiple times may be a problem, and switching back and forth between forms. > In addition, there seems to be no space between multiple strings written to the same class, resulting in an unknown QQ operator In addition, the appendrawcommands method uses UTF-8, which is strange to PDF. > Generatesigned document is most likely to cause great damage because it assumes that it can process text documents like PDF This is not generally the case
Results PDF questions
Finally, the sample result PDF provided by the op can identify some actual implemented problems:
>Comparing the bytes of the two documents (report_08_05_23. PDF and signed_report_08_05_23. PDF), we find that there are many unnecessary changes. At first glance, especially replacing some bytes with question marks This is because bytearrayoutputstream ToString () can easily manipulate the document and eventually change it back to byte []
For example Compare bytearrayoutputstream JavaDocs for tostring()
* <p> This method always replaces malformed-input and unmappable-character * sequences with the default replacement string for the platform's * default character set. The {@linkplain java.nio.charset.CharsetDecoder} * class should be used when more control over the decoding process is * required.
Some byte values do not represent characters in the platform default character set, so they are converted to unicode replacement character, and finally converted to byte [] to 0x3f (ASCII code of question mark) This change will kill the compressed content of the content stream and the image stream
To solve this problem, you must use byte and byte [] operations instead of string operations. > Stream 8 0 references itself in its xobject resource, which may cause any PDF viewer to throw Please don't do this cycle
Signature container problem
The signature could not be verified Therefore, it is also reviewed
>Checking the signature container shows that it is wrong: even though the signature is ADBE pkcs7. Detached, the signature container embeds data The reason for looking at the code is obvious:
CMSSignedData sigData = generator.generate(msg,true);
The true parameter requires the BC to embed MSG data. > After starting to view the signature code, another problem becomes visible: the MSG data above is not just a summary, they are already signatures:
Signature signature = Signature.getInstance(algorithm,BC); signature.initSign(privateKey); signature.update(docForSign.getBytes()); CMSTypedData msg = new CMSProcessableByteArray(signature.sign());
This is an error because the signerinfo generator created later is used to create the actual signature
Edit: adobe reader still does not accept signatures after the previously mentioned problems have been fixed or at least resolved So let's look at the code and:
Hash value calculation problem
OP constructs this byterange value
String finalByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";
Set later
String docFirstPart = docString.substring(0,offsetContentStart + 1); String docSecondPart = docString.substring(offsetContentEnd - 1);
1 and - 1 are intended so that these document sections also include < 1 And > contain signature bytes However, Op also uses these strings to construct signature data:
String docForSign = docFirstPart.concat(docSecondPart);
This is an error. Signed bytes do not contain < and > Therefore, the hash value calculated later is also wrong, and adobe reader has good reason to assume that the document has been manipulated
Having said that, there are other problems at regular intervals:
Offset and length update issues
OP insert byte range is as follows:
String interimByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]"; int byteRangeLengthDifference = interimByteRange.length() - initByteRange.length(); offsetContentStart = offsetContentStart + byteRangeLengthDifference; offsetContentEnd = offsetContentEnd + byteRangeLengthDifference; String finalByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]"; byteRangeLengthDifference += interimByteRange.length() - finalByteRange.length(); //Replace the ByteRange docString = docString.replace(initByteRange,finalByteRange);
Each of offsetcontentstart or offsetcontentend will be slightly lower than a certain 10 ^ n over a period of time and will be slightly higher later This line
byteRangeLengthDifference += interimByteRange.length() - finalByteRange.length();
Trying to make up for this, the finalbyterange (eventually inserted into the document) still contains uncorrected values
In a similar manner, the representation of the XRef begins to be inserted like this
docString = docString.substring(0,startxrefOffset).concat("startxref\n".concat(Integer.toString(xrefOffset))).concat("\n%%EOF\n");
It may also be longer than before, so that the byte range (pre calculated) does not cover the entire document
In addition, use the text search of the entire document to find the offset of the relevant PDF object
offsetContentStart = (documentOutputStream.toString().indexOf("Contents <") + 10 - 1); offsetContentEnd = (documentOutputStream.toString().indexOf("000000>") + 7); ... int xrefOffset = docString.indexOf("xref"); ... int startxrefOffset = docString.indexOf("startxref");
The generic file will fail For example If there is a previous signature in the document, it is likely to identify the wrong index like this