package mkl.testarea.pdfbox2.sign; import java.awt.geom.Rectangle2D; import java.io.ByteArrayInputStream; 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.nio.file.Files; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.MessageDigest; import java.security.PrivateKey; import java.security.Security; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.pdfbox.Loader; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSString; import org.apache.pdfbox.examples.signature.CreateVisibleSignature; import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDNonTerminalField; import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.DERSet; import org.bouncycastle.asn1.cms.Attribute; import org.bouncycastle.asn1.cms.AttributeTable; import org.bouncycastle.asn1.cms.CMSAttributes; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; import org.bouncycastle.cms.CMSAbsentContent; import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.CMSSignedDataGenerator; import org.bouncycastle.cms.CMSTypedData; import org.bouncycastle.cms.DefaultSignedAttributeTableGenerator; import org.bouncycastle.cms.SignerInfoGeneratorBuilder; import org.bouncycastle.crypto.util.PrivateKeyFactory; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; import org.bouncycastle.operator.bc.BcDigestCalculatorProvider; import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder; import org.bouncycastle.util.Store; import org.junit.BeforeClass; import org.junit.Test; import mkl.testarea.pdfbox2.extract.BoundingBoxFinder; /** * @author mkl */ public class CreateSignature { final static File RESULT_FOLDER = new File("target/test-outputs", "sign"); public static final String KEYSTORE = "keystores/demo-rsa2048.ks"; public static final char[] PASSWORD = "demo-rsa2048".toCharArray(); public static KeyStore ks = null; public static PrivateKey pk = null; public static Certificate[] chain = null; @BeforeClass public static void setUpBeforeClass() throws Exception { RESULT_FOLDER.mkdirs(); BouncyCastleProvider bcp = new BouncyCastleProvider(); Security.addProvider(bcp); //Security.insertProviderAt(bcp, 1); ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(new FileInputStream(KEYSTORE), PASSWORD); String alias = (String) ks.aliases().nextElement(); pk = (PrivateKey) ks.getKey(alias, PASSWORD); chain = ks.getCertificateChain(alias); } /** * <a href="http://stackoverflow.com/questions/41767351/create-pkcs7-signature-from-file-digest"> * Create pkcs7 signature from file digest * </a> * <p> * This test uses the OP's own <code>sign</code> method: {@link #signBySnox(InputStream)}. * There are small errors in it, so the result is rejected by verification. These errors * are corrected in {@link #signWithSeparatedHashing(InputStream)} which is tested in * {@link #testSignWithSeparatedHashing()}. * </p> */ @Test public void testSignWithSeparatedHashingLikeSnox() throws IOException { try ( InputStream resource = getClass().getResourceAsStream("test.pdf"); OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "testSignedLikeSnox.pdf")); PDDocument pdDocument = Loader.loadPDF(resource) ) { sign(pdDocument, result, data -> signBySnox(data)); } } /** * <a href="http://stackoverflow.com/questions/41767351/create-pkcs7-signature-from-file-digest"> * Create pkcs7 signature from file digest * </a> * <p> * This test uses a fixed version of the OP's <code>sign</code> method: * {@link #signWithSeparatedHashing(InputStream)}. Here the errors from * {@link #signBySnox(InputStream)} are corrected, so the result is not * rejected by verification anymore. * </p> */ @Test public void testSignWithSeparatedHashing() throws IOException { try ( InputStream resource = getClass().getResourceAsStream("test.pdf"); OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "testSignedWithSeparatedHashing.pdf")); PDDocument pdDocument = Loader.loadPDF(resource) ) { sign(pdDocument, result, data -> signWithSeparatedHashing(data)); } } /** * <a href="http://stackoverflow.com/questions/41767351/create-pkcs7-signature-from-file-digest"> * Create pkcs7 signature from file digest * </a> * <p> * A minimal signing frame work merely requiring a {@link SignatureInterface} * instance. * </p> */ void sign(PDDocument document, OutputStream output, SignatureInterface signatureInterface) throws IOException { PDSignature signature = new PDSignature(); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); signature.setName("Example User"); signature.setLocation("Los Angeles, CA"); signature.setReason("Testing"); signature.setSignDate(Calendar.getInstance()); document.addSignature(signature); ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output); // invoke external signature service byte[] cmsSignature = signatureInterface.sign(externalSigning.getContent()); // set signature bytes received from the service externalSigning.setSignature(cmsSignature); } /** * <a href="http://stackoverflow.com/questions/41767351/create-pkcs7-signature-from-file-digest"> * Create pkcs7 signature from file digest * </a> * <p> * The OP's own <code>sign</code> method which has some errors. These * errors are fixed in {@link #signWithSeparatedHashing(InputStream)}. * </p> */ public byte[] signBySnox(InputStream content) throws IOException { // testSHA1WithRSAAndAttributeTable try { MessageDigest md = MessageDigest.getInstance("SHA1", "BC"); List<Certificate> certList = new ArrayList<Certificate>(); CMSTypedData msg = new CMSProcessableByteArray(IOUtils.toByteArray(content)); certList.addAll(Arrays.asList(chain)); Store<?> certs = new JcaCertStore(certList); CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); Attribute attr = new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(md.digest(IOUtils.toByteArray(content))))); ASN1EncodableVector v = new ASN1EncodableVector(); v.add(attr); SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider()) .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v))); AlgorithmIdentifier sha1withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1withRSA"); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); InputStream in = new ByteArrayInputStream(chain[0].getEncoded()); X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in); gen.addSignerInfoGenerator(builder.build( new BcRSAContentSignerBuilder(sha1withRSA, new DefaultDigestAlgorithmIdentifierFinder().find(sha1withRSA)) .build(PrivateKeyFactory.createKey(pk.getEncoded())), new JcaX509CertificateHolder(cert))); gen.addCertificates(certs); CMSSignedData s = gen.generate(new CMSAbsentContent(), false); return new CMSSignedData(msg, s.getEncoded()).getEncoded(); } catch (Exception e) { e.printStackTrace(); throw new IOException(e); } } /** * <a href="http://stackoverflow.com/questions/41767351/create-pkcs7-signature-from-file-digest"> * Create pkcs7 signature from file digest * </a> * <p> * The OP's <code>sign</code> method after fixing some errors. The * OP's original method is {@link #signBySnox(InputStream)}. The * errors were * </p> * <ul> * <li>multiple attempts at reading the {@link InputStream} parameter; * <li>convoluted creation of final CMS container. * </ul> * <p> * Additionally this method uses SHA256 instead of SHA-1. * </p> */ public byte[] signWithSeparatedHashing(InputStream content) throws IOException { try { // Digest generation step MessageDigest md = MessageDigest.getInstance("SHA256", "BC"); byte[] digest = md.digest(IOUtils.toByteArray(content)); // Separate signature container creation step List<Certificate> certList = Arrays.asList(chain); JcaCertStore certs = new JcaCertStore(certList); CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); Attribute attr = new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(digest))); ASN1EncodableVector v = new ASN1EncodableVector(); v.add(attr); SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider()) .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v))); AlgorithmIdentifier sha256withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA"); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); InputStream in = new ByteArrayInputStream(chain[0].getEncoded()); X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in); gen.addSignerInfoGenerator(builder.build( new BcRSAContentSignerBuilder(sha256withRSA, new DefaultDigestAlgorithmIdentifierFinder().find(sha256withRSA)) .build(PrivateKeyFactory.createKey(pk.getEncoded())), new JcaX509CertificateHolder(cert))); gen.addCertificates(certs); CMSSignedData s = gen.generate(new CMSAbsentContent(), false); return s.getEncoded(); } catch (Exception e) { e.printStackTrace(); throw new IOException(e); } } /** * <a href="https://stackoverflow.com/questions/49894319/lock-dictionary-in-signature-field-is-the-reason-of-broken-signature-after-sig"> * “Lock” dictionary in signature field is the reason of broken signature after signing * </a> * <p> * This test shows how to properly sign signatures in a field with a * signature Lock dictionary. In particular important is the addition * of FieldMDP transform data to the signature value as here is done * in {@link #signExistingFieldWithLock(PDDocument, OutputStream, SignatureInterface)}. * </p> */ @Test public void testSignWithLocking() throws IOException { try ( InputStream resource = getClass().getResourceAsStream("test.pdf"); OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "testFieldWithLocking.pdf")); PDDocument pdDocument = Loader.loadPDF(resource) ) { PDAcroForm acroForm = pdDocument.getDocumentCatalog().getAcroForm(); if (acroForm == null) { acroForm = new PDAcroForm(pdDocument); pdDocument.getDocumentCatalog().setAcroForm(acroForm); } PDSignatureField signatureField = new PDSignatureField(acroForm); acroForm.getFields().add(signatureField); signatureField.getWidgets().get(0).setPage(pdDocument.getPage(0)); pdDocument.getPage(0).getAnnotations().add(signatureField.getWidgets().get(0)); signatureField.getWidgets().get(0).setRectangle(new PDRectangle(100, 600, 300, 200)); setLock(signatureField, acroForm); pdDocument.save(result); } try ( InputStream resource = new FileInputStream(new File(RESULT_FOLDER, "testFieldWithLocking.pdf")); OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "testSignedWithLocking.pdf")); PDDocument pdDocument = Loader.loadPDF(resource) ) { signExistingFieldWithLock(pdDocument, result, data -> signWithSeparatedHashing(data)); } } /** * <p> * A minimal signing frame work merely requiring a {@link SignatureInterface} * instance signing an existing field. * </p> * @see #testSignWithLocking() */ void signExistingFieldWithLock(PDDocument document, OutputStream output, SignatureInterface signatureInterface) throws IOException { PDSignatureField signatureField = document.getSignatureFields().get(0); 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); } 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); ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output); // invoke external signature service byte[] cmsSignature = signatureInterface.sign(externalSigning.getContent()); // set signature bytes received from the service externalSigning.setSignature(cmsSignature); } /** * <a href="https://stackoverflow.com/questions/49894319/lock-dictionary-in-signature-field-is-the-reason-of-broken-signature-after-sig"> * “Lock” dictionary in signature field is the reason of broken signature after signing * </a> * <p> * This code originally was in the OP's SigningUtils class. * </p> */ public static void setLock(PDSignatureField pdSignatureField, PDAcroForm acroForm) { COSDictionary lockDict = new COSDictionary(); lockDict.setItem(COS_NAME_ACTION, COS_NAME_ALL); lockDict.setItem(COSName.TYPE, COS_NAME_SIG_FIELD_LOCK); pdSignatureField.getCOSObject().setItem(COS_NAME_LOCK, lockDict); } public static final COSName COS_NAME_LOCK = COSName.getPDFName("Lock"); public static final COSName COS_NAME_ACTION = COSName.getPDFName("Action"); public static final COSName COS_NAME_ALL = COSName.getPDFName("All"); public static final COSName COS_NAME_SIG_FIELD_LOCK = COSName.getPDFName("SigFieldLock"); /** * <a href="https://stackoverflow.com/questions/50224181/pdfbox-2-0-8-issue-while-signing-document"> * PDFBox 2.0.8 issue while signing document * </a> * <p> * This test checks the OP's code which essentially is a copy of the * PDFBox example class CreateVisibleSignature with an extra method. * </p> * <p> * The cause was that the OP chose a PDDocument.addSignature overload for * external signing while implementing otherwise internal signing. Choosing * an appropriate overload fixed the problem. * </p> */ @Test public void signLikeIperezmel78() throws IOException, GeneralSecurityException { try ( InputStream resource = getClass().getResourceAsStream("test.pdf")) { PDDocument document = Loader.loadPDF(resource); byte[] bytes = VisibleSignature.sign(document, KEYSTORE, PASSWORD, "/mkl/testarea/pdfbox2/content/Willi-1.jpg"); Files.write(new File(RESULT_FOLDER, "test-signedLikeIperezmel78.pdf").toPath(), bytes); } } /** * <a href="https://stackoverflow.com/questions/52757037/how-to-generate-pkcs7-signature-from-digest"> * How to generate PKCS#7 signature from digest? * </a> * <p> * Like {@link #sign(PDDocument, OutputStream, SignatureInterface)}, merely * the subfilter now indicates a PAdES signature, not a legacy ISO 32000-1 * signature. The generated signature is invalid as it does not have an ESS * signing certificate attribute. * </p> * @see #testSignPAdESWithSeparatedHashing() */ void signPAdES(PDDocument document, OutputStream output, SignatureInterface signatureInterface) throws IOException { PDSignature signature = new PDSignature(); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setSubFilter(PDSignature.SUBFILTER_ETSI_CADES_DETACHED); signature.setName("Example User"); signature.setLocation("Los Angeles, CA"); signature.setReason("Testing"); signature.setSignDate(Calendar.getInstance()); document.addSignature(signature); ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output); // invoke external signature service byte[] cmsSignature = signatureInterface.sign(externalSigning.getContent()); // set signature bytes received from the service externalSigning.setSignature(cmsSignature); } /** * <a href="https://stackoverflow.com/questions/52757037/how-to-generate-pkcs7-signature-from-digest"> * How to generate PKCS#7 signature from digest? * </a> * <p> * This test is like {@link #testSignWithSeparatedHashing()}, merely the * subfilter now indicates a PAdES signature, not a legacy ISO 32000-1 * signature. The generated signature is invalid as it does not have an * ESS signing certificate attribute required by PAdES. * </p> */ @Test public void testSignPAdESWithSeparatedHashing() throws IOException { try ( InputStream resource = getClass().getResourceAsStream("test.pdf"); OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "testSignedPAdESWithSeparatedHashing.pdf")); PDDocument pdDocument = Loader.loadPDF(resource) ) { signPAdES(pdDocument, result, data -> signWithSeparatedHashing(data)); } } /** * <a href="https://stackoverflow.com/questions/58427451/how-to-apply-digital-signature-image-at-bottom-left-position-in-the-last-page-of"> * How to apply digital signature image at bottom left position in the last page of pdf using pdfbox? * </a> * <br/> * <a href="http://www.orimi.com/pdf-test.pdf"> * pdf-test.pdf * </a> * <p> * As the OP found out himself, the `BoundingBoxFinder` coordinates * could not be used as is in the `CreateVisibleSignature`. This test * demonstrates the required transformation with the example document * apparently used by the OP. * </p> */ @Test public void signLikeHemantPdfTest() throws IOException, GeneralSecurityException { File documentFile = new File("src/test/resources/mkl/testarea/pdfbox2/sign/pdf-test.pdf"); File signedDocumentFile = new File(RESULT_FOLDER, "pdf-test-signedLikeHemant.pdf"); Rectangle2D boundingBox; PDRectangle mediaBox; try ( PDDocument document = Loader.loadPDF(documentFile) ) { PDPage pdPage = document.getPage(0); BoundingBoxFinder boundingBoxFinder = new BoundingBoxFinder(pdPage); boundingBoxFinder.processPage(pdPage); boundingBox = boundingBoxFinder.getBoundingBox(); mediaBox = pdPage.getMediaBox(); } CreateVisibleSignature signing = new CreateVisibleSignature(ks, PASSWORD.clone()); try ( InputStream imageStream = getClass().getResourceAsStream("/mkl/testarea/pdfbox2/content/Willi-1.jpg")) { signing.setVisibleSignDesigner(documentFile.getPath(), (int)boundingBox.getX(), (int)(mediaBox.getUpperRightY() - boundingBox.getY()), -50, imageStream, 1); } signing.setVisibleSignatureProperties("name", "location", "Security", 0, 1, true); signing.setExternalSigning(false); signing.signPDF(documentFile, signedDocumentFile, null); } /** * <a href="https://stackoverflow.com/questions/58427451/how-to-apply-digital-signature-image-at-bottom-left-position-in-the-last-page-of"> * How to apply digital signature image at bottom left position in the last page of pdf using pdfbox? * </a> * <p> * As the OP found out himself, the `BoundingBoxFinder` coordinates * could not be used as is in the `CreateVisibleSignature`. This test * demonstrates the required transformation with an arbitrary example * document. * </p> */ @Test public void signLikeHemantTest() throws IOException, GeneralSecurityException { File documentFile = new File("src/test/resources/mkl/testarea/pdfbox2/sign/test.pdf"); File signedDocumentFile = new File(RESULT_FOLDER, "test-signedLikeHemant.pdf"); Rectangle2D boundingBox; PDRectangle mediaBox; try ( PDDocument document = Loader.loadPDF(documentFile) ) { PDPage pdPage = document.getPage(0); BoundingBoxFinder boundingBoxFinder = new BoundingBoxFinder(pdPage); boundingBoxFinder.processPage(pdPage); boundingBox = boundingBoxFinder.getBoundingBox(); mediaBox = pdPage.getMediaBox(); } CreateVisibleSignature signing = new CreateVisibleSignature(ks, PASSWORD.clone()); try ( InputStream imageStream = getClass().getResourceAsStream("/mkl/testarea/pdfbox2/content/Willi-1.jpg")) { signing.setVisibleSignDesigner(documentFile.getPath(), (int)boundingBox.getX(), (int)(mediaBox.getUpperRightY() - boundingBox.getY()), -50, imageStream, 1); } signing.setVisibleSignatureProperties("name", "location", "Security", 0, 1, true); signing.setExternalSigning(false); signing.signPDF(documentFile, signedDocumentFile, null); } /** * <a href="https://stackoverflow.com/questions/59027388/signing-pdf-with-multiple-signature-fields-using-pdfbox-2-0-17"> * Signing PDF with multiple signature fields using PDFBox 2.0.17 * </a> * <br/> * <a href="https://github.com/lawrencelkp/pdfboxtest"> * Fillable-2s.pdf * </a> * <p> * {@link #signExistingFieldWithLock(PDDocument, OutputStream, SignatureInterface)} * successfully signs the first signature field of the document. * </p> */ @Test public void testSignFillable2sWithLocking() throws IOException { try ( InputStream resource = getClass().getResourceAsStream("Fillable-2s.pdf"); OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "Fillable-2s-SignedWithLocking.pdf")); PDDocument pdDocument = Loader.loadPDF(resource) ) { signExistingFieldWithLock(pdDocument, result, data -> signWithSeparatedHashing(data)); } } /** * <p> * A minimal signing frame work merely requiring a {@link SignatureInterface} * instance signing an existing field and actually locking fields the transform * requires to be locked. * </p> * @see #signExistingFieldWithLock(PDDocument, OutputStream, SignatureInterface) */ void signAndLockExistingFieldWithLock(PDDocument document, OutputStream output, SignatureInterface signatureInterface) throws IOException { PDSignatureField signatureField = document.getSignatureFields().get(0); 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); ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output); // invoke external signature service byte[] cmsSignature = signatureInterface.sign(externalSigning.getContent()); // set signature bytes received from the service externalSigning.setSignature(cmsSignature); } /** @see #signAndLockExistingFieldWithLock(PDDocument, OutputStream, SignatureInterface) */ 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; } /** * <a href="https://stackoverflow.com/questions/59027388/signing-pdf-with-multiple-signature-fields-using-pdfbox-2-0-17"> * Signing PDF with multiple signature fields using PDFBox 2.0.17 * </a> * <br/> * <a href="https://github.com/lawrencelkp/pdfboxtest"> * Fillable-2s.pdf * </a> * <p> * {@link #signAndLockExistingFieldWithLock(PDDocument, OutputStream, SignatureInterface)} * successfully signs the first signature field of the document while at the same time * making the locked fields read only and locking their widgets. * </p> */ @Test public void testSignAndLockFillable2sWithLocking() throws IOException { try ( InputStream resource = getClass().getResourceAsStream("Fillable-2s.pdf"); OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "Fillable-2s-SignedAndLockedWithLocking.pdf")); PDDocument pdDocument = Loader.loadPDF(resource) ) { pdDocument.getDocumentCatalog().getAcroForm().getField("Text1").setValue("Text1"); signAndLockExistingFieldWithLock(pdDocument, result, data -> signWithSeparatedHashing(data)); } } /** * <a href="https://issues.apache.org/jira/browse/PDFBOX-4702"> * signature verification in Adobe products * </a> * <br/> * <a href="https://issues.apache.org/jira/secure/attachment/12987251/pdfbox-4702-unsigned.pdf"> * pdfbox-4702-unsigned.pd * </a> * <p> * {@link #signAndLockExistingFieldWithLock(PDDocument, OutputStream, SignatureInterface)} * successfully signs the signature field of the document. * </p> */ @Test public void testSignAndLockPdfbox4702UnsignedWithLocking() throws IOException { try ( InputStream resource = getClass().getResourceAsStream("pdfbox-4702-unsigned.pdf"); OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "pdfbox-4702-unsigned-SignedAndLockedWithLocking.pdf")); PDDocument pdDocument = Loader.loadPDF(resource) ) { signAndLockExistingFieldWithLock(pdDocument, result, data -> signWithSeparatedHashing(data)); } } }