diff --git a/imixs-archive-signature/README.md b/imixs-archive-signature/README.md index 6b2b201f..10639011 100644 --- a/imixs-archive-signature/README.md +++ b/imixs-archive-signature/README.md @@ -83,21 +83,15 @@ The SignatureAdapter does throw a PluginException in case not certificate for th #### Configuration The adapter creates a new certificate (autocreate=true) or signs the document with the root certificate if no user certificate exists (rootsignature=true) - true - false - order.pdf - -**autocreate** -If autocreate=true than in case no certificate for the current user exists, the SignatureAdaper will create a certificate on the fly. +**filepattern** -**rootsignature** +A regular expression to filter the attachments do be signed by the plugin. -If no no certificate for the current user exists, a document will be signed with the root certificate. + order.pdf -**filepattern** -A regular expression to filter the attachments do be signed by the plugin. If no file pattern is set, only PDF files will be signed +If no file pattern is set, only PDF files will be signed (^.+\\.([pP][dD][fF])$). @@ -107,6 +101,23 @@ The following example will sign all pdf files with the sufix 'order.pdf' You can find general details about how to use an Adapter with Imixs-Workflow [here](https://www.imixs.org/doc/core/adapter-api.html). + +**rootsignature** + +The document will be signed with the root certificate. + + true + +If not set, a signature based on the current user will be generated. + +**autocreate** + +If autocreate=true than in case no certificate for the current user exists, the SignatureAdaper will create a certificate on the fly. + + true + + + ## The CA Service diff --git a/imixs-archive-signature/docs/README.md b/imixs-archive-signature/docs/README.md index f79b47ac..0990c120 100644 --- a/imixs-archive-signature/docs/README.md +++ b/imixs-archive-signature/docs/README.md @@ -59,16 +59,20 @@ After the successful DNS challenge you will find the chain and key files at: /etc/letsencrypt/archive/foo.com/ -**3. Import the Root Certificate** +Read the next section to import the root certificate. + +### Import a Root Certificate + +If you have obtained a valid certificate you can import the certificate into an existing keystore using the keytool. The following section explains the procedure. The keytool does not allow to import multiple public and private .pem certificates directly. So first you’ll need to add all .pem files to a PKCS12 archive. This can be done with the OpenSSL tool: $ sudo openssl pkcs12 -export \ - -in /etc/letsencrypt/live/foo.com/cert.pem \ - -inkey /etc/letsencrypt/live/foo.com/privkey.pem \ + -in foo.com/cert.pem \ + -inkey foo.com/privkey.pem \ -out foo.com.p12 \ -name foo.com \ - -CAfile /etc/letsencrypt/live/foo.com/fullchain.pem \ + -CAfile foo.com/fullchain.pem \ -caname "Let's Encrypt Authority X3" \ -password pass:changeit @@ -87,6 +91,7 @@ Next you can import the certificates into your keystore: -destkeystore imixs.jks \ -alias foo.com +Replace the password 'changeit' with password of your keystore! To verify the content of the keystore run: @@ -95,7 +100,7 @@ To verify the content of the keystore run: -### Create Certificate with the Root Certificate +### Createing a Certificate from a Root Certificate If you have imported a root certificate into your keystore you can create new keypairs signed by the root CA. diff --git a/imixs-archive-signature/src/main/java/org/imixs/archive/signature/pdf/SigningService.java b/imixs-archive-signature/src/main/java/org/imixs/archive/signature/pdf/SigningService.java index 870a7170..1291c7d2 100644 --- a/imixs-archive-signature/src/main/java/org/imixs/archive/signature/pdf/SigningService.java +++ b/imixs-archive-signature/src/main/java/org/imixs/archive/signature/pdf/SigningService.java @@ -27,7 +27,6 @@ import java.awt.geom.Rectangle2D; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.security.KeyStoreException; @@ -78,12 +77,11 @@ import org.imixs.archive.signature.pdf.cert.CertificateVerificationException; import org.imixs.archive.signature.pdf.cert.SigningException; import org.imixs.archive.signature.pdf.util.SigUtils; -import org.imixs.workflow.FileData; /** * The SignatureService provides methods to sign a PDF document on a X509 - * certificate. The service adds a digital signature and also a visual signature. - * The method 'signPDF' expects a Imixs FileData object containing + * certificate. The service adds a digital signature and also a visual + * signature. The method 'signPDF' expects a Imixs FileData object containing * the content of the PDF file to be signed. *

* The service expects the existence of a valid X509 certificate stored in the @@ -134,13 +132,12 @@ public class SigningService { keytool -storepass 123456 -storetype PKCS12 -keystore file.p12 -genkey -alias client -keyalg RSA } * - * @param documentFile - File to be signed - * @param alias - the alias used to sign the document. The alias should - * be listed in the keystore. - * - * - * @param signatureImage - image of visible signature - * @return signed FileData object + * @param inputFileData A byte array containing the source PDF document. + * @param certAlias Certificate alias name to be used for signing + * @param certPassword optional private key password + * @param externalSigning optional boolean flag to trigger an external signing + * process + * @return A byte array containing the singed PDF document * * @throws SigningException * @throws CertificateVerificationException @@ -148,25 +145,19 @@ public class SigningService { * * */ - public FileData signPDF(FileData inputFileData, String certAlias, String certPassword, File signatureImage) + public byte[] signPDF(byte[] inputFileData, String certAlias, String certPassword, boolean externalSigning) throws CertificateVerificationException, SigningException { - logger.info("......signPDF '" + inputFileData.getName() + "' ..."); - - String name = inputFileData.getName(); - String substring = name.substring(0, name.lastIndexOf('.')); - String signedFileName = substring + "_signed.pdf"; - // Set the signature rectangle // Although PDF coordinates start from the bottom, humans start from the top. // So a human would want to position a signature (x,y) units from the // top left of the displayed page, and the field has a horizontal width and a // vertical height // regardless of page rotation. - Rectangle2D humanRect = new Rectangle2D.Float(50, 660, 170, 50); + // Rectangle2D humanRect = new Rectangle2D.Float(50, 660, 170, 50); - FileData signedFileData = createSignedPDF(inputFileData, signedFileName, certAlias, certPassword, humanRect, - "Signature1", signatureImage, false); + byte[] signedFileData = signPDF(inputFileData, certAlias, certPassword, externalSigning, null, "Signature1", + null); return signedFileData; @@ -175,27 +166,28 @@ public FileData signPDF(FileData inputFileData, String certAlias, String certPas /** * Sign pdf file and create new file that ends with "_signed.pdf". * - * @param inputFile The source pdf document file. - * @param signedFile The file to be signed. - * @param alias Certificate alias name to be used for signing + * @param inputFileData A byte array containing the source PDF document. + * @param certAlias Certificate alias name to be used for signing + * @param certPassword optional private key password + * @param externalSigning optional boolean flag to trigger an external + * signing process * @param humanRect rectangle from a human viewpoint (coordinates start * at top left) * @param tsaUrl optional TSA url * @param signatureFieldName optional name of an existing (unsigned) signature * field - * @param externalSigning optional boolean flag to trigger an external - * signing process + * @return A byte array containing the singed PDF document * @throws CertificateVerificationException * @throws SigningException */ - private FileData createSignedPDF(FileData inputFileData, String signedFileName, String certAlias, - String certPassword, Rectangle2D humanRect, String signatureFieldName, File imageFile, - boolean externalSigning) throws CertificateVerificationException, SigningException { + public byte[] signPDF(byte[] inputFileData, String certAlias, String certPassword, boolean externalSigning, + Rectangle2D humanRect, String signatureFieldName, byte[] imageFile) + throws CertificateVerificationException, SigningException { SignatureOptions signatureOptions = null; byte[] signedContent = null; - if (inputFileData == null || inputFileData.getContent().length == 0) { + if (inputFileData == null || inputFileData.length == 0) { throw new SigningException("empty file data"); } @@ -203,8 +195,7 @@ private FileData createSignedPDF(FileData inputFileData, String signedFileName, // ByteArrayOutputStream // try (FileOutputStream fos = new FileOutputStream(signedFile); PDDocument doc // = PDDocument.load(inputFileData.getContent())) { - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); - PDDocument doc = PDDocument.load(inputFileData.getContent())) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); PDDocument doc = PDDocument.load(inputFileData)) { int accessPermissions = SigUtils.getMDPPermission(doc); if (accessPermissions == 1) { throw new SigningException( @@ -220,12 +211,18 @@ private FileData createSignedPDF(FileData inputFileData, String signedFileName, PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm(); PDRectangle rect = null; - // sign a PDF with an existing empty signature, as created by the - // CreateEmptySignatureForm example. + // If the PDF contains an existing empty signature, as created by the + // CreateEmptySignatureForm example we can reuse it here if (acroForm != null) { - pdSignature = findExistingSignature(acroForm, signatureFieldName); - if (pdSignature != null) { - rect = acroForm.getField(signatureFieldName).getWidgets().get(0).getRectangle(); + try { + pdSignature = findExistingSignature(acroForm, signatureFieldName); + if (pdSignature != null) { + rect = acroForm.getField(signatureFieldName).getWidgets().get(0).getRectangle(); + } + } catch (IllegalStateException ise) { + // we can not use this signature field + logger.warning("signature " + signatureFieldName + " already exists: " + ise.getMessage()); + signatureFieldName = signatureFieldName + ".1"; } } @@ -234,7 +231,7 @@ private FileData createSignedPDF(FileData inputFileData, String signedFileName, pdSignature = new PDSignature(); } - if (rect == null) { + if (rect == null && humanRect != null) { rect = createSignatureRectangle(doc, humanRect); } @@ -310,12 +307,12 @@ private FileData createSignedPDF(FileData inputFileData, String signedFileName, // register signature dictionary and sign interface signatureOptions = new SignatureOptions(); - // create visual signature if imageFile exists.... - if (imageFile.exists()) { + // create visual signature if a signing rect object exists.... + if (rect != null) { signatureOptions.setVisualSignature( createVisualSignatureTemplate(doc, 0, rect, pdSignature, imageFile, certificateChain)); } else { - logger.warning("Signature Image '" + imageFile.getPath() + "' does not exist. No Image will be added!"); + logger.info("...Signature Image not provided, no VisualSignature will be added!"); } signatureOptions.setPage(0); doc.addSignature(pdSignature, signature, signatureOptions); @@ -348,9 +345,8 @@ private FileData createSignedPDF(FileData inputFileData, String signedFileName, IOUtils.closeQuietly(signatureOptions); } - // return the new singed FileData object - FileData signedFileData = new FileData(signedFileName, signedContent, "application/pdf", null); - return signedFileData; + // return the new singed content + return signedContent; } private PDRectangle createSignatureRectangle(PDDocument doc, Rectangle2D humanRect) { @@ -395,7 +391,7 @@ private PDRectangle createSignatureRectangle(PDDocument doc, Rectangle2D humanRe // create a template PDF document with empty signature and return it as a // stream. private InputStream createVisualSignatureTemplate(PDDocument srcDoc, int pageNum, PDRectangle rect, - PDSignature signature, File imageFile, Certificate[] certificateChain) throws IOException { + PDSignature signature, byte[] imageFile, Certificate[] certificateChain) throws IOException { try (PDDocument doc = new PDDocument()) { PDPage page = new PDPage(srcDoc.getPage(pageNum).getMediaBox()); doc.addPage(page); @@ -468,8 +464,13 @@ private InputStream createVisualSignatureTemplate(PDDocument srcDoc, int pageNum // save and restore graphics if the image is too large and needs to be scaled cs.saveGraphicsState(); cs.transform(Matrix.getScaleInstance(0.25f, 0.25f)); - PDImageXObject img = PDImageXObject.createFromFileByExtension(imageFile, doc); + // PDImageXObject img = PDImageXObject.createFromFileByExtension(imageFile, + // doc); + // create image form image byte array + // if (imageFile != null) { + PDImageXObject img = PDImageXObject.createFromByteArray(doc, imageFile, null); cs.drawImage(img, 0, 0); + // } cs.restoreGraphicsState(); } @@ -511,8 +512,17 @@ private InputStream createVisualSignatureTemplate(PDDocument srcDoc, int pageNum } } - // Find an existing signature (assumed to be empty). You will usually not need - // this. + /** + * This method verifies if for a given sigFieldName a signature already exists. + * If so, the method throws a IllegalStateException. In that case, the + * signatureField can not be used for another signature and a new empty + * signatureField have to be created. + * + * @see singPDF + * @param acroForm + * @param sigFieldName + * @return a PDSignature if exits + */ private PDSignature findExistingSignature(PDAcroForm acroForm, String sigFieldName) { PDSignature signature = null; PDSignatureField signatureField; diff --git a/imixs-archive-signature/src/main/java/org/imixs/archive/signature/workflow/SignatureAdapter.java b/imixs-archive-signature/src/main/java/org/imixs/archive/signature/workflow/SignatureAdapter.java index db7b0080..b59dd7db 100644 --- a/imixs-archive-signature/src/main/java/org/imixs/archive/signature/workflow/SignatureAdapter.java +++ b/imixs-archive-signature/src/main/java/org/imixs/archive/signature/workflow/SignatureAdapter.java @@ -1,6 +1,6 @@ package org.imixs.archive.signature.workflow; -import java.io.File; +import java.awt.geom.Rectangle2D; import java.io.IOException; import java.security.InvalidKeyException; import java.security.KeyStoreException; @@ -75,7 +75,7 @@ public class SignatureAdapter implements SignalAdapter { @Inject WorkflowService workflowService; - + @Inject X509ProfileHandler x509ProfileHandler; @@ -118,37 +118,46 @@ public ItemCollection execute(ItemCollection document, ItemCollection event) thr if (filePatternMatcher.matcher(fileName).find()) { // yes! start signing.... - // compute alias validate existence of certificate - String certAlias = workflowService.getUserName(); - logger.info("......signing " + fileName + " by '" + certAlias + "'..."); - // we assume an empty password for certificate String certPassword = ""; - - // test if a certificate exits.... - if (!caService.existsCertificate(certAlias)) { - if (autocreate) { - // lookup the x509 data form the x509ProfileHandler - ItemCollection x509Profile= x509ProfileHandler.findX509Profile(certAlias); - // create new certificate.... - caService.createCertificate(certAlias,x509Profile); - } else { - // try to fetch the root certificate - if (rootsignature && rootCertAlias.isPresent()) { - certAlias = rootCertAlias.get(); - // set SIGNATURE_ROOTCERT_PASSWORD - if (rootCertPassword.isPresent()) { - certPassword = rootCertPassword.get(); - } - } else { - throw new CertificateVerificationException("certificate for alias '" + certAlias - + "' not found. Missing default certificate alias (SIGNATURE_KEYSTORE_DEFAULT_ALIAS)!"); - } + String certAlias = null; + + // Test if the a signature with the root certificate is requested + if (rootsignature && rootCertAlias.isPresent()) { + certAlias = rootCertAlias.get(); + // set SIGNATURE_ROOTCERT_PASSWORD + if (rootCertPassword.isPresent()) { + certPassword = rootCertPassword.get(); } + // test existence of default certificate if (!caService.existsCertificate(certAlias)) { throw new ProcessingErrorException(this.getClass().getSimpleName(), "SIGNING_ERROR", - "No certificate exists for user '" + certAlias + "'"); + "Root certificate '" + certAlias + "' does not exist!"); + } + logger.info("......signing " + fileName + " with root certificate '" + certAlias + "'..."); + } else { + // signature with user certificate.... + // compute alias validate existence of certificate + certAlias = workflowService.getUserName(); + logger.info("......signing " + fileName + " by '" + certAlias + "'..."); + + // test if a certificate exits.... + if (!caService.existsCertificate(certAlias)) { + if (autocreate) { + // lookup the x509 data form the x509ProfileHandler + ItemCollection x509Profile = x509ProfileHandler.findX509Profile(certAlias); + // create new certificate.... + caService.createCertificate(certAlias, x509Profile); + } else { + throw new CertificateVerificationException( + "certificate for alias '" + certAlias + "' not found."); + } + // test existence of default certificate + if (!caService.existsCertificate(certAlias)) { + throw new ProcessingErrorException(this.getClass().getSimpleName(), "SIGNING_ERROR", + "No certificate exists for user '" + certAlias + "'"); + } } } @@ -162,16 +171,24 @@ public ItemCollection execute(ItemCollection document, ItemCollection event) thr sourceContent = fileData.getContent(); } - // Path path = Paths.get(fileName); - // Files.write(path, sourceContent); - // File filePDFSource = new File(fileName); - File fileSignatureImage = new File("/opt/imixs-keystore/" + certAlias + ".jpg"); - FileData signedFileData = signatureService.signPDF(fileData, certAlias, certPassword, - fileSignatureImage); + byte[] signedContent=null; + // in case of a rootsignature we do not generate a signature visual! + if (rootsignature) { + signedContent = signatureService.signPDF(sourceContent, certAlias, certPassword,false); + } else { + Rectangle2D humanRect = new Rectangle2D.Float(50, 660, 170, 50); + + // create signature withvisual + //File fileSignatureImage = new File("/opt/imixs-keystore/" + certAlias + ".jpg"); + signedContent = signatureService.signPDF(sourceContent, certAlias, certPassword,false,humanRect,"Signature1", + null); + } // ad the signed pdf file to the workitem + FileData signedFileData = new FileData(fileName, signedContent,"application/pdf", null); + document.addFileData(signedFileData); - logger.info("......signing " + fileName + " completed!"); + logger.info("......signing " + fileName + " completed!"); } } }