xiongzhu 4 lat temu
rodzic
commit
69f71a21fe
27 zmienionych plików z 6580 dodań i 0 usunięć
  1. BIN
      o2server/x_base_core_project/src/main/resources/myKeystore.p12
  2. BIN
      o2server/x_base_core_project/src/main/resources/sig.jpg
  3. 37 0
      o2server/x_processplatform_assemble_surface/pom.xml
  4. 368 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/ActionDocToPDFWorkOrWorkCompleted.java
  5. 71 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CMSProcessableInputStream.java
  6. 225 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateEmbeddedTimeStamp.java
  7. 90 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateEmptySignatureForm.java
  8. 210 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateSignature.java
  9. 170 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateSignatureBase.java
  10. 175 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateSignedTimeStamp.java
  11. 469 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateVisibleSignature.java
  12. 532 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateVisibleSignature2.java
  13. 664 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/ShowSignature.java
  14. 383 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/SigUtils.java
  15. 172 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/TSAClient.java
  16. 135 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/ValidationTimeStamp.java
  17. 341 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/CRLVerifier.java
  18. 40 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/CertificateVerificationException.java
  19. 65 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/CertificateVerificationResult.java
  20. 486 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/CertificateVerifier.java
  21. 610 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/OcspHelper.java
  22. 48 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/RevokedCertificateException.java
  23. 25 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/package.html
  24. 601 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/validation/AddValidationInformation.java
  25. 466 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/validation/CertInformationCollector.java
  26. 165 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/validation/CertInformationHelper.java
  27. 32 0
      o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/validation/CertificateProccessingException.java

BIN
o2server/x_base_core_project/src/main/resources/myKeystore.p12


BIN
o2server/x_base_core_project/src/main/resources/sig.jpg


+ 37 - 0
o2server/x_processplatform_assemble_surface/pom.xml

@@ -48,6 +48,43 @@
 			<groupId>o2oa</groupId>
 			<artifactId>x_general_core_entity</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>com.github.kevinsawicki</groupId>
+			<artifactId>http-request</artifactId>
+			<version>6.0</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.pdfbox</groupId>
+			<artifactId>pdfbox</artifactId>
+			<version>3.0.0-RC1</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.pdfbox</groupId>
+			<artifactId>pdfbox-tools</artifactId>
+			<version>3.0.0-RC1</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.pdfbox</groupId>
+			<artifactId>xmpbox</artifactId>
+			<version>3.0.0-RC1</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.pdfbox</groupId>
+			<artifactId>preflight</artifactId>
+			<version>3.0.0-RC1</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.lucene</groupId>
+			<artifactId>lucene-core</artifactId>
+			<version>7.7.2</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.lucene</groupId>
+			<artifactId>lucene-analyzers-common</artifactId>
+			<version>7.7.2</version>
+		</dependency>
 	</dependencies>
 	<build>
 		<plugins>

+ 368 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/ActionDocToPDFWorkOrWorkCompleted.java

@@ -0,0 +1,368 @@
+package com.x.processplatform.assemble.surface.jaxrs.attachment;
+
+import com.github.kevinsawicki.http.HttpRequest;
+import com.google.gson.JsonElement;
+import com.x.base.core.container.EntityManagerContainer;
+import com.x.base.core.container.factory.EntityManagerContainerFactory;
+import com.x.base.core.entity.annotation.CheckPersistType;
+import com.x.base.core.project.annotation.FieldDescribe;
+import com.x.base.core.project.config.Config;
+import com.x.base.core.project.config.ProcessPlatform;
+import com.x.base.core.project.config.ProcessPlatform.WorkCompletedExtensionEvent;
+import com.x.base.core.project.config.ProcessPlatform.WorkExtensionEvent;
+import com.x.base.core.project.config.StorageMapping;
+import com.x.base.core.project.connection.CipherConnectionAction;
+import com.x.base.core.project.exception.ExceptionAccessDenied;
+import com.x.base.core.project.exception.ExceptionEntityNotExist;
+import com.x.base.core.project.gson.GsonPropertyObject;
+import com.x.base.core.project.http.ActionResult;
+import com.x.base.core.project.http.EffectivePerson;
+import com.x.base.core.project.jaxrs.WoId;
+import com.x.base.core.project.tools.DefaultCharset;
+import com.x.base.core.project.tools.DocumentTools;
+import com.x.processplatform.assemble.surface.Business;
+import com.x.processplatform.assemble.surface.ThisApplication;
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.CreateVisibleSignature2;
+import com.x.processplatform.core.entity.content.Attachment;
+import com.x.processplatform.core.entity.content.Work;
+import com.x.processplatform.core.entity.content.WorkCompleted;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.util.TempFile;
+import org.apache.tika.Tika;
+
+import javax.imageio.ImageIO;
+import java.awt.geom.Rectangle2D;
+import java.io.*;
+import java.security.KeyStore;
+import java.util.*;
+
+class ActionDocToPDFWorkOrWorkCompleted extends BaseAction {
+
+    ActionResult<Wo> execute(EffectivePerson effectivePerson, String workOrWorkCompleted, JsonElement jsonElement)
+            throws Exception {
+        ActionResult<Wo> result = new ActionResult<>();
+        Wi wi = this.convertToWrapIn(jsonElement, Wi.class);
+        Work work = null;
+        WorkCompleted workCompleted = null;
+        Wo wo = new Wo();
+
+        try (EntityManagerContainer emc = EntityManagerContainerFactory.instance().create()) {
+            Business business = new Business(emc);
+            work = emc.find(workOrWorkCompleted, Work.class);
+            if (null == work) {
+                workCompleted = emc.flag(workOrWorkCompleted, WorkCompleted.class);
+            }
+            if ((null == work) && (null == workCompleted)) {
+                throw new ExceptionEntityNotExist(workOrWorkCompleted, Work.class);
+            }
+            if (!business.readableWithWorkOrWorkCompleted(effectivePerson, workOrWorkCompleted,
+                    new ExceptionEntityNotExist(workOrWorkCompleted))) {
+                throw new ExceptionAccessDenied(effectivePerson);
+            }
+        }
+        if (null != work) {
+            wo = this.work(effectivePerson, wi, work);
+        } else {
+            wo = this.workCompleted(effectivePerson, wi, workCompleted);
+        }
+
+        result.setData(wo);
+        return result;
+
+    }
+
+    private byte[] workConvert(EffectivePerson effectivePerson, Wi wi, String application, String process,
+                               String activity, String job) throws Exception {
+        byte[] bytes = null;
+        Optional<WorkExtensionEvent> event;
+        if (wi.getFileName().toLowerCase().endsWith(OFD_ATT_KEY)) {
+            event = Config.processPlatform().getExtensionEvents().getWorkDocToOfdEvents()
+                    .bind(application, process, activity);
+        } else {
+            event = Config.processPlatform().getExtensionEvents().getWorkDocToWordEvents()
+                    .bind(application, process, activity);
+        }
+        if (event.isPresent()) {
+            bytes = this.workExtensionService(effectivePerson, wi.getContent(), event.get(), job);
+        } else {
+            if (StringUtils.equals(ProcessPlatform.DOCTOWORDTYPE_CLOUD, Config.processPlatform().getDocToWordType())) {
+                bytes = DocumentTools.docToWord(wi.getFileName(), wi.getContent());
+            } else {
+                bytes = this.local(wi);
+            }
+        }
+        return bytes;
+    }
+
+    private byte[] workExtensionService(EffectivePerson effectivePerson, String content, WorkExtensionEvent event, String job)
+            throws Exception {
+        byte[] bytes = null;
+        Req req = new Req();
+        req.setContent(content);
+        req.setPerson(effectivePerson.getDistinguishedName());
+        req.setJob(job);
+        if (StringUtils.isNotEmpty(event.getCustom())) {
+            bytes = ThisApplication.context().applications().postQueryBinary(event.getCustom(), event.getUrl(), req);
+        } else {
+            bytes = CipherConnectionAction.postBinary(effectivePerson.getDebugger(), event.getUrl(), req);
+        }
+        return bytes;
+    }
+
+    private byte[] workCompletedConvert(EffectivePerson effectivePerson, Wi wi, String application, String process, String job)
+            throws Exception {
+        byte[] bytes = null;
+        Optional<WorkCompletedExtensionEvent> event;
+        if (wi.getFileName().toLowerCase().endsWith(OFD_ATT_KEY)) {
+            event = Config.processPlatform().getExtensionEvents()
+                    .getWorkCompletedDocToOfdEvents().bind(application, process);
+        } else {
+            event = Config.processPlatform().getExtensionEvents()
+                    .getWorkCompletedDocToWordEvents().bind(application, process);
+        }
+        if (event.isPresent()) {
+            bytes = this.workCompletedExtensionService(effectivePerson, wi.getContent(), event.get(), job);
+        } else {
+            if (StringUtils.equals(ProcessPlatform.DOCTOWORDTYPE_CLOUD, Config.processPlatform().getDocToWordType())) {
+                bytes = DocumentTools.docToWord(wi.getFileName(), wi.getContent());
+            } else {
+                bytes = this.local(wi);
+            }
+        }
+        return bytes;
+    }
+
+    private byte[] workCompletedExtensionService(EffectivePerson effectivePerson, String content,
+                                                 WorkCompletedExtensionEvent event, String job) throws Exception {
+        byte[] bytes = null;
+        Req req = new Req();
+        req.setContent(content);
+        req.setPerson(effectivePerson.getDistinguishedName());
+        req.setJob(job);
+        if (StringUtils.isNotEmpty(event.getCustom())) {
+            bytes = ThisApplication.context().applications().postQueryBinary(event.getCustom(), event.getUrl(), req);
+        } else {
+            bytes = CipherConnectionAction.postBinary(effectivePerson.getDebugger(), event.getUrl(), req);
+        }
+        return bytes;
+    }
+
+    private Wo work(EffectivePerson effectivePerson, Wi wi, Work work) throws Exception {
+        String person = effectivePerson.isCipher() ? work.getCreatorPerson() : effectivePerson.getDistinguishedName();
+        byte[] bytes = this.workConvert(effectivePerson, wi, work.getApplication(), work.getProcess(),
+                work.getActivity(), work.getJob());
+        try (EntityManagerContainer emc = EntityManagerContainerFactory.instance().create()) {
+            List<Attachment> attachments = emc.listEqual(Attachment.class, Attachment.job_FIELDNAME, work.getJob());
+            Attachment attachment = null;
+            for (Attachment o : attachments) {
+                if (StringUtils.equalsIgnoreCase(wi.getSite(), o.getSite())
+                        && StringUtils.equalsIgnoreCase(o.getName(), wi.getFileName())) {
+                    attachment = o;
+                    break;
+                }
+            }
+            if (null != attachment) {
+                StorageMapping mapping = ThisApplication.context().storageMappings().get(Attachment.class,
+                        attachment.getStorage());
+                emc.beginTransaction(Attachment.class);
+                attachment.updateContent(mapping, bytes, wi.getFileName());
+                attachment.setType((new Tika()).detect(bytes, wi.getFileName()));
+                attachment.setLastUpdatePerson(person);
+                attachment.setLastUpdateTime(new Date());
+                emc.check(attachment, CheckPersistType.all);
+                emc.commit();
+            } else {
+                StorageMapping mapping = ThisApplication.context().storageMappings().random(Attachment.class);
+                emc.beginTransaction(Attachment.class);
+                attachment = new Attachment();
+                attachment.setCompleted(false);
+                attachment.setPerson(person);
+                attachment.setLastUpdatePerson(person);
+                attachment.setSite(wi.getSite());
+                // 用于判断目录的值
+                attachment.setWorkCreateTime(work.getCreateTime());
+                attachment.setApplication(work.getApplication());
+                attachment.setProcess(work.getProcess());
+                attachment.setJob(work.getJob());
+                attachment.setActivity(work.getActivity());
+                attachment.setActivityName(work.getActivityName());
+                attachment.setActivityToken(work.getActivityToken());
+                attachment.setActivityType(work.getActivityType());
+                attachment.saveContent(mapping, bytes, wi.getFileName());
+                attachment.setType((new Tika()).detect(bytes, wi.getFileName()));
+                emc.persist(attachment, CheckPersistType.all);
+                emc.commit();
+            }
+            Wo wo = new Wo();
+            wo.setId(attachment.getId());
+            return wo;
+        }
+    }
+
+    private Wo workCompleted(EffectivePerson effectivePerson, Wi wi, WorkCompleted workCompleted) throws Exception {
+        String person = effectivePerson.isCipher() ? workCompleted.getCreatorPerson()
+                : effectivePerson.getDistinguishedName();
+        byte[] bytes = this.workCompletedConvert(effectivePerson, wi, workCompleted.getApplication(),
+                workCompleted.getProcess(), workCompleted.getJob());
+        try (EntityManagerContainer emc = EntityManagerContainerFactory.instance().create()) {
+            List<Attachment> attachments = emc.listEqual(Attachment.class, Attachment.job_FIELDNAME,
+                    workCompleted.getJob());
+            Attachment attachment = null;
+            for (Attachment o : attachments) {
+                if (StringUtils.equalsIgnoreCase(wi.getSite(), o.getSite())
+                        && StringUtils.equalsIgnoreCase(o.getName(), wi.getFileName())) {
+                    attachment = o;
+                    break;
+                }
+            }
+            if (null != attachment) {
+                StorageMapping mapping = ThisApplication.context().storageMappings().get(Attachment.class,
+                        attachment.getStorage());
+                emc.beginTransaction(Attachment.class);
+                attachment.updateContent(mapping, bytes, wi.getFileName());
+                attachment.setType((new Tika()).detect(bytes, wi.getFileName()));
+                attachment.setLastUpdatePerson(person);
+                attachment.setLastUpdateTime(new Date());
+                emc.check(attachment, CheckPersistType.all);
+                emc.commit();
+            } else {
+                StorageMapping mapping = ThisApplication.context().storageMappings().random(Attachment.class);
+                emc.beginTransaction(Attachment.class);
+                attachment = new Attachment();
+                attachment.setCompleted(false);
+                attachment.setPerson(person);
+                attachment.setLastUpdatePerson(person);
+                attachment.setSite(wi.getSite());
+                // 用于判断目录的值
+                attachment.setWorkCreateTime(workCompleted.getCreateTime());
+                attachment.setApplication(workCompleted.getApplication());
+                attachment.setProcess(workCompleted.getProcess());
+                attachment.setJob(workCompleted.getJob());
+                attachment.setActivity(workCompleted.getActivity());
+                attachment.setActivityName(workCompleted.getActivityName());
+                attachment.saveContent(mapping, bytes, wi.getFileName());
+                attachment.setType((new Tika()).detect(bytes, wi.getFileName()));
+                emc.persist(attachment, CheckPersistType.all);
+                emc.commit();
+            }
+            Wo wo = new Wo();
+            wo.setId(attachment.getId());
+            return wo;
+        }
+    }
+
+    private byte[] local(Wi wi) throws Exception {
+        try (POIFSFileSystem fs = new POIFSFileSystem();
+             InputStream is = new ByteArrayInputStream(wi.getContent().getBytes(DefaultCharset.name_iso_utf_8));
+             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            fs.createDocument(is, "WordDocument");
+            fs.writeFilesystem(out);
+
+            File pdfFile = TempFile.createTempFile("", ".pdf");
+            File signedPdfFile = TempFile.createTempFile("", ".pdf");
+            HttpRequest.post("http://192.168.50.238/word2pdf")
+                    .accept("*/*")
+                    .part("file", "word.docx",
+                            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+                            new ByteArrayInputStream(out.toByteArray()))
+                    .receive(new FileOutputStream(pdfFile));
+
+
+            String password = "3edc#EDC";
+
+            KeyStore keystore = KeyStore.getInstance("PKCS12");
+            keystore.load(getClass().getResourceAsStream("myKeystore.p12"), password.toCharArray());
+
+
+            CreateVisibleSignature2 signing = new CreateVisibleSignature2(keystore, password.toCharArray());
+
+            signing.setImage(ImageIO.read(getClass().getResourceAsStream("sig.jpg")));
+            signing.setExternalSigning(false);
+
+            // 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(340, 500, 150, 150);
+
+            signing.signPDF(pdfFile, signedPdfFile, humanRect, null, "Signature1");
+
+            return FileUtils.readFileToByteArray(signedPdfFile);
+        }
+    }
+
+    public static class Wo extends WoId {
+    }
+
+    public static class Wi extends GsonPropertyObject {
+
+        @FieldDescribe("转换文件名.")
+        private String fileName;
+        @FieldDescribe("附件site.")
+        private String site;
+        @FieldDescribe("内容.")
+        private String content;
+
+        public String getFileName() throws Exception {
+            return StringUtils.isEmpty(fileName) ? Config.processPlatform().getDocToWordDefaultFileName() : fileName;
+        }
+
+        public void setSite(String site) {
+            this.site = site;
+        }
+
+        public void setFileName(String fileName) {
+            this.fileName = fileName;
+        }
+
+        public String getSite() throws Exception {
+            return StringUtils.isEmpty(site) ? Config.processPlatform().getDocToWordDefaultSite() : site;
+        }
+
+        public String getContent() {
+            return content;
+        }
+
+        public void setContent(String content) {
+            this.content = content;
+        }
+
+    }
+
+    public static class Req {
+
+        private String person;
+
+        private String content;
+
+        private String job;
+
+        public String getPerson() {
+            return person;
+        }
+
+        public void setPerson(String person) {
+            this.person = person;
+        }
+
+        public String getContent() {
+            return content;
+        }
+
+        public void setContent(String content) {
+            this.content = content;
+        }
+
+        public String getJob() {
+            return job;
+        }
+
+        public void setJob(String job) {
+            this.job = job;
+        }
+    }
+
+}

+ 71 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CMSProcessableInputStream.java

@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.apache.pdfbox.io.IOUtils;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSTypedData;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Wraps a InputStream into a CMSProcessable object for bouncy castle. It's a memory saving
+ * alternative to the {@link org.bouncycastle.cms.CMSProcessableByteArray CMSProcessableByteArray}
+ * class.
+ *
+ * @author Thomas Chojecki
+ */
+class CMSProcessableInputStream implements CMSTypedData
+{
+    private final InputStream in;
+    private final ASN1ObjectIdentifier contentType;
+
+    CMSProcessableInputStream(InputStream is)
+    {
+        this(new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()), is);
+    }
+
+    CMSProcessableInputStream(ASN1ObjectIdentifier type, InputStream is)
+    {
+        contentType = type;
+        in = is;
+    }
+
+    @Override
+    public Object getContent()
+    {
+        return in;
+    }
+
+    @Override
+    public void write(OutputStream out) throws IOException, CMSException
+    {
+        // read the content only one time
+        IOUtils.copy(in, out);
+        in.close();
+    }
+
+    @Override
+    public ASN1ObjectIdentifier getContentType()
+    {
+        return contentType;
+    }
+}

+ 225 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateEmbeddedTimeStamp.java

@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
+import org.apache.pdfbox.util.Hex;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSSignedData;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * An example for timestamp-signing a PDF for PADeS-Specification. The document will only be changed
+ * in its existing signature by a signed timestamp (A timestamp and the Hash-Value of the document
+ * are signed by a Time Stamp Authority (TSA)).
+ *
+ * This method only changes the unsigned parameters of a signature, so that it is kept valid.
+ *
+ * Use case: sign offline to avoid zero-day attacks against the signing machine. Once the signature
+ * is there and the pdf is transferred to a network connected machine, one is likely to want to add
+ * a timestamp. (Ralf Hauser)
+ *
+ * @author Alexis Suter
+ */
+public class CreateEmbeddedTimeStamp
+{
+    private final String tsaUrl;
+    private PDDocument document;
+    private PDSignature signature;
+    private byte[] changedEncodedSignature;
+
+    public CreateEmbeddedTimeStamp(String tsaUrl)
+    {
+        this.tsaUrl = tsaUrl;
+    }
+
+    /**
+     * Embeds the given PDF file with signed timestamp(s). Alters the original file on disk.
+     * 
+     * @param file the PDF file to sign and to overwrite
+     * @throws IOException
+     */
+    public void embedTimeStamp(File file) throws IOException
+    {
+        embedTimeStamp(file, file);
+    }
+
+    /**
+     * Embeds signed timestamp(s) into existing signatures of the given document
+     * 
+     * @param inFile The pdf file possibly containing signatures
+     * @param outFile Where the changed document will be saved
+     * @throws IOException
+     */
+    public void embedTimeStamp(File inFile, File outFile) throws IOException
+    {
+        if (inFile == null || !inFile.exists())
+        {
+            throw new FileNotFoundException("Document for signing does not exist");
+        }
+
+        // sign
+        try (PDDocument doc = Loader.loadPDF(inFile))
+        {
+            document = doc;
+            processTimeStamping(inFile, outFile);
+        }
+    }
+
+    /**
+     * Processes the time-stamping of the signature.
+     * 
+     * @param inFile The existing PDF file
+     * @param outFile Where the new file will be written to
+     * @throws IOException
+     */
+    private void processTimeStamping(File inFile, File outFile) throws IOException
+    {
+        try
+        {
+            byte[] documentBytes = Files.readAllBytes(inFile.toPath());
+            processRelevantSignatures(documentBytes);
+
+            if (changedEncodedSignature == null)
+            {
+                throw new IllegalStateException("No signature");
+            }
+            try (FileOutputStream output = new FileOutputStream(outFile))
+            {
+                embedNewSignatureIntoDocument(documentBytes, output);
+            }
+        }
+        catch (IOException | NoSuchAlgorithmException | CMSException e)
+        {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Create changed Signature with embedded TimeStamp from TSA
+     * 
+     * @param documentBytes byte[] of the input file
+     * @throws IOException
+     * @throws CMSException
+     * @throws NoSuchAlgorithmException
+     */
+    private void processRelevantSignatures(byte[] documentBytes)
+            throws IOException, CMSException, NoSuchAlgorithmException
+    {
+        signature = SigUtils.getLastRelevantSignature(document);
+        if (signature == null)
+        {
+            return;
+        }
+
+        byte[] sigBlock = signature.getContents(documentBytes);
+        CMSSignedData signedData = new CMSSignedData(sigBlock);
+
+        System.out.println("INFO: Byte Range: " + Arrays.toString(signature.getByteRange()));
+
+        if (tsaUrl != null && tsaUrl.length() > 0)
+        {
+            ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl);
+            signedData = validation.addSignedTimeStamp(signedData);
+        }
+
+        byte[] newEncoded = Hex.getBytes(signedData.getEncoded());
+        int maxSize = signature.getByteRange()[2] - signature.getByteRange()[1];
+        System.out.println(
+                "INFO: New Signature has Size: " + newEncoded.length + " maxSize: " + maxSize);
+
+        if (newEncoded.length > maxSize - 2)
+        {
+            throw new IOException(
+                    "New Signature is too big for existing Signature-Placeholder. Max Place: "
+                    + maxSize);
+        }
+        else
+        {
+            changedEncodedSignature = newEncoded;
+        }
+    }
+
+    /**
+     * Embeds the new signature into the document, by copying the rest of the document
+     * 
+     * @param docBytes byte array of the document
+     * @param output target, where the file will be written
+     * @throws IOException
+     */
+    private void embedNewSignatureIntoDocument(byte[] docBytes, OutputStream output)
+            throws IOException
+    {
+        int[] byteRange = signature.getByteRange();
+        output.write(docBytes, byteRange[0], byteRange[1] + 1);
+        output.write(changedEncodedSignature);
+        int addingLength = byteRange[2] - byteRange[1] - 2 - changedEncodedSignature.length;
+        byte[] zeroes = Hex.getBytes(new byte[(addingLength + 1) / 2]);
+        output.write(zeroes);
+        output.write(docBytes, byteRange[2] - 1, byteRange[3] + 1);
+    }
+
+    public static void main(String[] args) throws IOException
+    {
+        if (args.length != 3)
+        {
+            usage();
+            System.exit(1);
+        }
+
+        String tsaUrl = null;
+        for (int i = 0; i < args.length; i++)
+        {
+            if ("-tsa".equals(args[i]))
+            {
+                i++;
+                if (i >= args.length)
+                {
+                    usage();
+                    System.exit(1);
+                }
+                tsaUrl = args[i];
+            }
+        }
+
+        File inFile = new File(args[0]);
+        System.out.println("Input File: " + args[0]);
+        String name = inFile.getName();
+        String substring = name.substring(0, name.lastIndexOf('.'));
+
+        File outFile = new File(inFile.getParent(), substring + "_eTs.pdf");
+        System.out.println("Output File: " + outFile.getAbsolutePath());
+
+        // Embed TimeStamp
+        CreateEmbeddedTimeStamp signing = new CreateEmbeddedTimeStamp(tsaUrl);
+        signing.embedTimeStamp(inFile, outFile);
+    }
+
+    private static void usage()
+    {
+        System.err.println("usage: java " + CreateEmbeddedTimeStamp.class.getName() + " "
+                + "<pdf_to_sign>\n" + "mandatory option:\n"
+                + "  -tsa <url>    sign timestamp using the given TSA server\n");
+    }
+}

+ 90 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateEmptySignatureForm.java

@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDResources;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
+import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
+import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
+
+import java.io.IOException;
+
+/**
+ * An example of creating an AcroForm and an empty signature field from scratch.
+ * 
+ * An actual signature can be added by clicking on it in Adobe Reader.
+ * 
+ */
+public final class CreateEmptySignatureForm
+{
+    private CreateEmptySignatureForm()
+    {
+    }
+    
+    public static void main(String[] args) throws IOException
+    {
+        // Create a new document with an empty page.
+        try (PDDocument document = new PDDocument())
+        {
+            PDPage page = new PDPage(PDRectangle.A4);
+            document.addPage(page);
+
+            // Adobe Acrobat uses Helvetica as a default font and
+            // stores that under the name '/Helv' in the resources dictionary
+            PDFont font = PDType1Font.HELVETICA;
+            PDResources resources = new PDResources();
+            resources.put(COSName.getPDFName("Helv"), font);
+
+            // Add a new AcroForm and add that to the document
+            PDAcroForm acroForm = new PDAcroForm(document);
+            document.getDocumentCatalog().setAcroForm(acroForm);
+
+            // Add and set the resources and default appearance at the form level
+            acroForm.setDefaultResources(resources);
+
+            // Acrobat sets the font size on the form level to be
+            // auto sized as default. This is done by setting the font size to '0'
+            String defaultAppearanceString = "/Helv 0 Tf 0 g";
+            acroForm.setDefaultAppearance(defaultAppearanceString);
+            // --- end of general AcroForm stuff ---
+
+            // Create empty signature field, it will get the name "Signature1"
+            PDSignatureField signatureField = new PDSignatureField(acroForm);
+            PDAnnotationWidget widget = signatureField.getWidgets().get(0);
+            PDRectangle rect = new PDRectangle(50, 650, 200, 50);
+            widget.setRectangle(rect);
+            widget.setPage(page);
+
+            // see thread from PDFBox users mailing list 17.2.2021 - 19.2.2021
+            // https://mail-archives.apache.org/mod_mbox/pdfbox-users/202102.mbox/thread
+            widget.setPrinted(true);
+
+            page.getAnnotations().add(widget);
+
+            acroForm.getFields().add(signatureField);
+
+            document.save(args[0]);
+        }
+    }
+}

+ 210 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateSignature.java

@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
+
+import java.io.*;
+import java.security.*;
+import java.security.cert.CertificateException;
+import java.util.Calendar;
+
+/**
+ * An example for signing a PDF with bouncy castle.
+ * A keystore can be created with the java keytool, for example:
+ *
+ * {@code keytool -genkeypair -storepass 123456 -storetype pkcs12 -alias test -validity 365
+ *        -v -keyalg RSA -keystore keystore.p12 }
+ *
+ * @author Thomas Chojecki
+ * @author Vakhtang Koroghlishvili
+ * @author John Hewson
+ */
+public class CreateSignature extends CreateSignatureBase
+{
+
+    /**
+     * Initialize the signature creator with a keystore and certificate password.
+     *
+     * @param keystore the pkcs12 keystore containing the signing certificate
+     * @param pin the password for recovering the key
+     * @throws KeyStoreException if the keystore has not been initialized (loaded)
+     * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
+     * @throws UnrecoverableKeyException if the given password is wrong
+     * @throws CertificateException if the certificate is not valid as signing time
+     * @throws IOException if no certificate could be found
+     */
+    public CreateSignature(KeyStore keystore, char[] pin)
+            throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, CertificateException, IOException
+    {
+        super(keystore, pin);
+    }
+
+    /**
+     * Signs the given PDF file. Alters the original file on disk.
+     * @param file the PDF file to sign
+     * @throws IOException if the file could not be read or written
+     */
+    public void signDetached(File file) throws IOException
+    {
+        signDetached(file, file, null);
+    }
+
+    /**
+     * Signs the given PDF file.
+     * @param inFile input PDF file
+     * @param outFile output PDF file
+     * @throws IOException if the input file could not be read
+     */
+    public void signDetached(File inFile, File outFile) throws IOException
+    {
+        signDetached(inFile, outFile, null);
+    }
+
+    /**
+     * Signs the given PDF file.
+     * @param inFile input PDF file
+     * @param outFile output PDF file
+     * @param tsaUrl optional TSA url
+     * @throws IOException if the input file could not be read
+     */
+    public void signDetached(File inFile, File outFile, String tsaUrl) throws IOException
+    {
+        if (inFile == null || !inFile.exists())
+        {
+            throw new FileNotFoundException("Document for signing does not exist");
+        }
+        
+        setTsaUrl(tsaUrl);
+
+        // sign
+        try (FileOutputStream fos = new FileOutputStream(outFile);
+                PDDocument doc = Loader.loadPDF(inFile))
+        {
+            signDetached(doc, fos);
+        }
+    }
+
+    public void signDetached(PDDocument document, OutputStream output)
+            throws IOException
+    {
+        int accessPermissions = SigUtils.getMDPPermission(document);
+        if (accessPermissions == 1)
+        {
+            throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
+        }     
+
+        // create signature dictionary
+        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");
+        // TODO extract the above details from the signing certificate? Reason as a parameter?
+
+        // the signing date, needed for valid signature
+        signature.setSignDate(Calendar.getInstance());
+
+        // Optional: certify 
+        if (accessPermissions == 0)
+        {
+            SigUtils.setMDPPermission(document, signature, 2);
+        }        
+
+        if (isExternalSigning())
+        {
+            document.addSignature(signature);
+            ExternalSigningSupport externalSigning =
+                    document.saveIncrementalForExternalSigning(output);
+            // invoke external signature service
+            byte[] cmsSignature = sign(externalSigning.getContent());
+            // set signature bytes received from the service
+            externalSigning.setSignature(cmsSignature);
+        }
+        else
+        {
+            SignatureOptions signatureOptions = new SignatureOptions();
+            // Size can vary, but should be enough for purpose.
+            signatureOptions.setPreferredSignatureSize(SignatureOptions.DEFAULT_SIGNATURE_SIZE * 2);
+            // register signature dictionary and sign interface
+            document.addSignature(signature, this, signatureOptions);
+
+            // write incremental (only for signing purpose)
+            document.saveIncremental(output);
+        }
+    }
+
+    public static void main(String[] args) throws IOException, GeneralSecurityException
+    {
+        if (args.length < 3)
+        {
+            usage();
+            System.exit(1);
+        }
+
+        String tsaUrl = null;
+        boolean externalSig = false;
+        for (int i = 0; i < args.length; i++)
+        {
+            if (args[i].equals("-tsa"))
+            {
+                i++;
+                if (i >= args.length)
+                {
+                    usage();
+                    System.exit(1);
+                }
+                tsaUrl = args[i];
+            }
+            if (args[i].equals("-e"))
+            {
+                externalSig = true;
+            }
+        }
+
+        // load the keystore
+        KeyStore keystore = KeyStore.getInstance("PKCS12");
+        char[] password = args[1].toCharArray(); // TODO use Java 6 java.io.Console.readPassword
+        keystore.load(new FileInputStream(args[0]), password);
+        // TODO alias command line argument
+
+        // sign PDF
+        CreateSignature signing = new CreateSignature(keystore, password);
+        signing.setExternalSigning(externalSig);
+
+        File inFile = new File(args[2]);
+        String name = inFile.getName();
+        String substring = name.substring(0, name.lastIndexOf('.'));
+
+        File outFile = new File(inFile.getParent(), substring + "_signed.pdf");
+        signing.signDetached(inFile, outFile, tsaUrl);
+    }
+
+    private static void usage()
+    {
+        System.err.println("usage: java " + CreateSignature.class.getName() + " " +
+                           "<pkcs12_keystore> <password> <pdf_to_sign>\n" + "" +
+                           "options:\n" +
+                           "  -tsa <url>    sign timestamp using the given TSA server\n" +
+                           "  -e            sign using external signature creation scenario");
+    }
+}

+ 170 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateSignatureBase.java

@@ -0,0 +1,170 @@
+/*
+ * Copyright 2015 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
+import org.bouncycastle.cert.jcajce.JcaCertStore;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.CMSSignedDataGenerator;
+import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Enumeration;
+
+public abstract class CreateSignatureBase implements SignatureInterface
+{
+    private PrivateKey privateKey;
+    private Certificate[] certificateChain;
+    private String tsaUrl;
+    private boolean externalSigning;
+
+    /**
+     * Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the
+     * signature.
+     *
+     * @param keystore is a pkcs12 keystore.
+     * @param pin is the pin for the keystore / private key
+     * @throws KeyStoreException if the keystore has not been initialized (loaded)
+     * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
+     * @throws UnrecoverableKeyException if the given password is wrong
+     * @throws CertificateException if the certificate is not valid as signing time
+     * @throws IOException if no certificate could be found
+     */
+    public CreateSignatureBase(KeyStore keystore, char[] pin)
+            throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException
+    {
+        // grabs the first alias from the keystore and get the private key. An
+        // alternative method or constructor could be used for setting a specific
+        // alias that should be used.
+        Enumeration<String> aliases = keystore.aliases();
+        String alias;
+        Certificate cert = null;
+        while (cert == null && aliases.hasMoreElements())
+        {
+            alias = aliases.nextElement();
+            setPrivateKey((PrivateKey) keystore.getKey(alias, pin));
+            Certificate[] certChain = keystore.getCertificateChain(alias);
+            if (certChain != null)
+            {
+                setCertificateChain(certChain);
+                cert = certChain[0];
+                if (cert instanceof X509Certificate)
+                {
+                    // avoid expired certificate
+                    ((X509Certificate) cert).checkValidity();
+
+                    SigUtils.checkCertificateUsage((X509Certificate) cert);
+                }
+            }
+        }
+
+        if (cert == null)
+        {
+            throw new IOException("Could not find certificate");
+        }
+    }
+
+    public final void setPrivateKey(PrivateKey privateKey)
+    {
+        this.privateKey = privateKey;
+    }
+
+    public final void setCertificateChain(final Certificate[] certificateChain)
+    {
+        this.certificateChain = certificateChain;
+    }
+
+    public Certificate[] getCertificateChain()
+    {
+        return certificateChain;
+    }
+
+    public void setTsaUrl(String tsaUrl)
+    {
+        this.tsaUrl = tsaUrl;
+    }
+
+    /**
+     * SignatureInterface sample implementation.
+     *<p>
+     * This method will be called from inside of the pdfbox and create the PKCS #7 signature.
+     * The given InputStream contains the bytes that are given by the byte range.
+     *<p>
+     * This method is for internal use only.
+     *<p>
+     * Use your favorite cryptographic library to implement PKCS #7 signature creation.
+     * If you want to create the hash and the signature separately (e.g. to transfer only the hash
+     * to an external application), read <a href="https://stackoverflow.com/questions/41767351">this
+     * answer</a> or <a href="https://stackoverflow.com/questions/56867465">this answer</a>.
+     *
+     * @throws IOException
+     */
+    @Override
+    public byte[] sign(InputStream content) throws IOException
+    {
+        // cannot be done private (interface)
+        try
+        {
+            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
+            X509Certificate cert = (X509Certificate) certificateChain[0];
+            ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey);
+            gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert));
+            gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
+            CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
+            CMSSignedData signedData = gen.generate(msg, false);
+            if (tsaUrl != null && tsaUrl.length() > 0)
+            {
+                ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl);
+                signedData = validation.addSignedTimeStamp(signedData);
+            }
+            return signedData.getEncoded();
+        }
+        catch (GeneralSecurityException | CMSException | OperatorCreationException e)
+        {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Set if external signing scenario should be used.
+     * If {@code false}, SignatureInterface would be used for signing.
+     * <p>
+     *     Default: {@code false}
+     * </p>
+     * @param externalSigning {@code true} if external signing should be performed
+     */
+    public void setExternalSigning(boolean externalSigning)
+    {
+        this.externalSigning = externalSigning;
+    }
+
+    public boolean isExternalSigning()
+    {
+        return externalSigning;
+    }
+}

+ 175 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateSignedTimeStamp.java

@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
+
+import java.io.*;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * An example for timestamp-signing a PDF for PADeS-Specification. The document will be extended by
+ * a signed TimeStamp (another kind of signature) (Signed TimeStamp and Hash-Value of the document
+ * are signed by a Time Stamp Authority (TSA)).
+ *
+ * @author Thomas Chojecki
+ * @author Vakhtang Koroghlishvili
+ * @author John Hewson
+ * @author Alexis Suter
+ */
+public class CreateSignedTimeStamp implements SignatureInterface
+{
+    private static final Log LOG = LogFactory.getLog(CreateSignedTimeStamp.class);
+    
+    private final String tsaUrl;
+
+    /**
+     * Initialize the signed timestamp creator
+     * 
+     * @param tsaUrl The url where TS-Request will be done.
+     */
+    public CreateSignedTimeStamp(String tsaUrl)
+    {
+        this.tsaUrl = tsaUrl;
+    }
+
+    /**
+     * Signs the given PDF file. Alters the original file on disk.
+     * 
+     * @param file the PDF file to sign
+     * @throws IOException if the file could not be read or written
+     */
+    public void signDetached(File file) throws IOException
+    {
+        signDetached(file, file);
+    }
+
+    /**
+     * Signs the given PDF file.
+     * 
+     * @param inFile input PDF file
+     * @param outFile output PDF file
+     * @throws IOException if the input file could not be read
+     */
+    public void signDetached(File inFile, File outFile) throws IOException
+    {
+        if (inFile == null || !inFile.exists())
+        {
+            throw new FileNotFoundException("Document for signing does not exist");
+        }
+
+        // sign
+        try (PDDocument doc = Loader.loadPDF(inFile);
+             FileOutputStream fos = new FileOutputStream(outFile))
+        {
+            signDetached(doc, fos);
+        }
+    }
+
+    /**
+     * Prepares the TimeStamp-Signature and starts the saving-process.
+     * 
+     * @param document given Pdf
+     * @param output Where the file will be written
+     * @throws IOException
+     */
+    public void signDetached(PDDocument document, OutputStream output) throws IOException
+    {
+        int accessPermissions = SigUtils.getMDPPermission(document);
+        if (accessPermissions == 1)
+        {
+            throw new IllegalStateException(
+                    "No changes to the document are permitted due to DocMDP transform parameters dictionary");
+        }
+
+        // create signature dictionary
+        PDSignature signature = new PDSignature();
+        signature.setType(COSName.DOC_TIME_STAMP);
+        signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
+        signature.setSubFilter(COSName.getPDFName("ETSI.RFC3161"));
+
+        // No certification allowed because /Reference not allowed in signature directory
+        // see ETSI EN 319 142-1 Part 1 and ETSI TS 102 778-4
+        // http://www.etsi.org/deliver/etsi_en%5C319100_319199%5C31914201%5C01.01.00_30%5Cen_31914201v010100v.pdf
+        // http://www.etsi.org/deliver/etsi_ts/102700_102799/10277804/01.01.01_60/ts_10277804v010101p.pdf
+
+        // register signature dictionary and sign interface
+        document.addSignature(signature, this);
+
+        // write incremental (only for signing purpose)
+        document.saveIncremental(output);
+    }
+
+    @Override
+    public byte[] sign(InputStream content) throws IOException
+    {
+        ValidationTimeStamp validation;
+        try
+        {
+            validation = new ValidationTimeStamp(tsaUrl);
+            return validation.getTimeStampToken(content);
+        }
+        catch (NoSuchAlgorithmException e)
+        {
+            LOG.error("Hashing-Algorithm not found for TimeStamping", e);
+        }
+        return new byte[] {};
+    }
+
+    public static void main(String[] args) throws IOException
+    {
+        if (args.length != 3)
+        {
+            usage();
+            System.exit(1);
+        }
+
+        String tsaUrl = null;
+        if ("-tsa".equals(args[1]))
+        {
+            tsaUrl = args[2];
+        }
+        else
+        {
+            usage();
+            System.exit(1);
+        }
+
+        // sign PDF
+        CreateSignedTimeStamp signing = new CreateSignedTimeStamp(tsaUrl);
+
+        File inFile = new File(args[0]);
+        String name = inFile.getName();
+        String substring = name.substring(0, name.lastIndexOf('.'));
+
+        File outFile = new File(inFile.getParent(), substring + "_timestamped.pdf");
+        signing.signDetached(inFile, outFile);
+    }
+
+    private static void usage()
+    {
+        System.err.println("usage: java " + CreateSignedTimeStamp.class.getName() + " "
+                + "<pdf_to_sign>\n" + "mandatory options:\n"
+                + "  -tsa <url>    sign timestamp using the given TSA server\n");
+    }
+}

+ 469 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateVisibleSignature.java

@@ -0,0 +1,469 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.io.IOUtils;
+import org.apache.pdfbox.io.MemoryUsageSetting;
+import org.apache.pdfbox.pdmodel.PDDocument;
+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.digitalsignature.SignatureOptions;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner;
+import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
+import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
+import org.apache.pdfbox.util.Hex;
+
+import java.io.*;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Calendar;
+
+/**
+ * This is an example for visual signing a pdf.
+
+ * @see CreateSignature
+ * @author Vakhtang Koroghlishvili
+ */
+public class CreateVisibleSignature extends CreateSignatureBase
+{
+    private SignatureOptions signatureOptions;
+    private PDVisibleSignDesigner visibleSignDesigner;
+    private final PDVisibleSigProperties visibleSignatureProperties = new PDVisibleSigProperties();
+    private boolean lateExternalSigning = false;
+    private MemoryUsageSetting memoryUsageSetting = MemoryUsageSetting.setupMainMemoryOnly();
+    private PDDocument doc = null;
+
+    public boolean isLateExternalSigning()
+    {
+        return lateExternalSigning;
+    }
+
+    /**
+     * Set late external signing. Enable this if you want to activate the demo code where the
+     * signature is kept and added in an extra step without using PDFBox methods. This is disabled
+     * by default.
+     *
+     * @param lateExternalSigning
+     */
+    public void setLateExternalSigning(boolean lateExternalSigning)
+    {
+        this.lateExternalSigning = lateExternalSigning;
+    }
+
+    /**
+     * Get the memory usage setting.
+     *
+     * @return the memory usage setting.
+     */
+    public MemoryUsageSetting getMemoryUsageSetting()
+    {
+        return memoryUsageSetting;
+    }
+
+    /**
+     * Set the memory usage setting.
+     *
+     * @param memoryUsageSetting the memory usage setting.
+     */
+    public void setMemoryUsageSetting(MemoryUsageSetting memoryUsageSetting)
+    {
+        this.memoryUsageSetting = memoryUsageSetting;
+    }
+
+    /**
+     * Open the PDF, create and set the visible signature designer for a new signature field.
+     * 
+     * @param filename path of the PDF file
+     * @param x position of the signature field
+     * @param y position of the signature field
+     * @param zoomPercent increase (positive value) or decrease (negative value) image with x percent.
+     * @param imageStream input stream of an image.
+     * @param page the signature should be placed on
+     * @throws IOException
+     */
+    public void setVisibleSignDesigner(String filename, int x, int y, int zoomPercent, 
+            InputStream imageStream, int page) 
+            throws IOException
+    {
+        doc = Loader.loadPDF(new File(filename), memoryUsageSetting);
+        visibleSignDesigner = new PDVisibleSignDesigner(doc, imageStream, page);
+        visibleSignDesigner.xAxis(x).yAxis(y).zoom(zoomPercent).adjustForRotation();
+    }
+
+    /**
+     * Set visible signature designer for an existing signature field.
+     * 
+     * @param zoomPercent increase (positive value) or decrease (negative value) image with x percent.
+     * @param imageStream input stream of an image.
+     * @throws IOException
+     */
+    public void setVisibleSignDesigner(int zoomPercent, InputStream imageStream) 
+            throws IOException
+    {
+        visibleSignDesigner = new PDVisibleSignDesigner(imageStream);
+        visibleSignDesigner.zoom(zoomPercent);
+    }
+
+    /**
+     * Set visible signature properties for new signature fields.
+     * 
+     * @param name
+     * @param location
+     * @param reason
+     * @param preferredSize
+     * @param page
+     * @param visualSignEnabled
+     */
+    public void setVisibleSignatureProperties(String name, String location, String reason, int preferredSize, 
+            int page, boolean visualSignEnabled)
+    {
+        visibleSignatureProperties.signerName(name).signerLocation(location).signatureReason(reason).
+                preferredSize(preferredSize).page(page).visualSignEnabled(visualSignEnabled).
+                setPdVisibleSignature(visibleSignDesigner);
+    }
+
+    /**
+     * Set visible signature properties for existing signature fields.
+     * 
+     * @param name
+     * @param location
+     * @param reason
+     * @param visualSignEnabled
+     */
+    public void setVisibleSignatureProperties(String name, String location, String reason,
+            boolean visualSignEnabled)
+    {
+        visibleSignatureProperties.signerName(name).signerLocation(location).signatureReason(reason).
+                visualSignEnabled(visualSignEnabled).setPdVisibleSignature(visibleSignDesigner);
+    }
+
+    /**
+     * Initialize the signature creator with a keystore (pkcs12) and pin that
+     * should be used for the signature.
+     *
+     * @param keystore is a pkcs12 keystore.
+     * @param pin is the pin for the keystore / private key
+     * @throws KeyStoreException if the keystore has not been initialized (loaded)
+     * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
+     * @throws UnrecoverableKeyException if the given password is wrong
+     * @throws CertificateException if the certificate is not valid as signing time
+     * @throws IOException if no certificate could be found
+     */
+    public CreateVisibleSignature(KeyStore keystore, char[] pin)
+            throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException
+    {
+        super(keystore, pin);
+    }
+
+    /**
+     * 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 tsaUrl optional TSA url
+     * @throws IOException
+     */
+    public void signPDF(File inputFile, File signedFile, String tsaUrl) throws IOException
+    {
+        this.signPDF(inputFile, signedFile, tsaUrl, null);
+    }
+
+    /**
+     * Sign pdf file and create new file that ends with "_signed.pdf".
+     *
+     * @param inputFile The source pdf document file. It will be opened if it hasn't been opened
+     * before in {@link #setVisibleSignDesigner(String, int, int, int, InputStream, int)}.
+     * @param signedFile The file to be signed.
+     * @param tsaUrl optional TSA url
+     * @param signatureFieldName optional name of an existing (unsigned) signature field
+     * @throws IOException
+     */
+    public void signPDF(File inputFile, File signedFile, String tsaUrl, String signatureFieldName) throws IOException
+    {
+        if (inputFile == null || !inputFile.exists())
+        {
+            throw new IOException("Document for signing does not exist");
+        }
+
+        setTsaUrl(tsaUrl);
+
+        // creating output document and prepare the IO streams.
+        if (doc == null)
+        {
+            doc = Loader.loadPDF(inputFile, memoryUsageSetting);
+        }
+
+        try (FileOutputStream fos = new FileOutputStream(signedFile))
+        {
+            int accessPermissions = SigUtils.getMDPPermission(doc);
+            if (accessPermissions == 1)
+            {
+                throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
+            }
+            // Note that PDFBox has a bug that visual signing on certified files with permission 2
+            // doesn't work properly, see PDFBOX-3699. As long as this issue is open, you may want to
+            // be careful with such files.
+
+            PDSignature signature;
+
+            // sign a PDF with an existing empty signature, as created by the CreateEmptySignatureForm example.
+            signature = findExistingSignature(doc, signatureFieldName);
+
+            if (signature == null)
+            {
+                // create signature dictionary
+                signature = new PDSignature();
+            }
+
+            // Optional: certify
+            // can be done only if version is at least 1.5 and if not already set
+            // doing this on a PDF/A-1b file fails validation by Adobe preflight (PDFBOX-3821)
+            // PDF/A-1b requires PDF version 1.4 max, so don't increase the version on such files.
+            if (doc.getVersion() >= 1.5f && accessPermissions == 0)
+            {
+                SigUtils.setMDPPermission(doc, signature, 2);
+            }
+
+            PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm(null);
+            if (acroForm != null && acroForm.getNeedAppearances())
+            {
+                // PDFBOX-3738 NeedAppearances true results in visible signature becoming invisible
+                // with Adobe Reader
+                if (acroForm.getFields().isEmpty())
+                {
+                    // we can safely delete it if there are no fields
+                    acroForm.getCOSObject().removeItem(COSName.NEED_APPEARANCES);
+                    // note that if you've set MDP permissions, the removal of this item
+                    // may result in Adobe Reader claiming that the document has been changed.
+                    // and/or that field content won't be displayed properly.
+                    // ==> decide what you prefer and adjust your code accordingly.
+                }
+                else
+                {
+                    System.out.println("/NeedAppearances is set, signature may be ignored by Adobe Reader");
+                }
+            }
+
+            // default filter
+            signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
+
+            // subfilter for basic and PAdES Part 2 signatures
+            signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
+
+            if (visibleSignatureProperties != null)
+            {
+                // this builds the signature structures in a separate document
+                visibleSignatureProperties.buildSignature();
+
+                signature.setName(visibleSignatureProperties.getSignerName());
+                signature.setLocation(visibleSignatureProperties.getSignerLocation());
+                signature.setReason(visibleSignatureProperties.getSignatureReason());
+            }
+
+            // the signing date, needed for valid signature
+            signature.setSignDate(Calendar.getInstance());
+
+            // do not set SignatureInterface instance, if external signing used
+            SignatureInterface signatureInterface = isExternalSigning() ? null : this;
+
+            // register signature dictionary and sign interface
+            if (visibleSignatureProperties != null && visibleSignatureProperties.isVisualSignEnabled())
+            {
+                signatureOptions = new SignatureOptions();
+                signatureOptions.setVisualSignature(visibleSignatureProperties.getVisibleSignature());
+                signatureOptions.setPage(visibleSignatureProperties.getPage() - 1);
+                doc.addSignature(signature, signatureInterface, signatureOptions);
+            }
+            else
+            {
+                doc.addSignature(signature, signatureInterface);
+            }
+
+            if (isExternalSigning())
+            {
+                ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
+                // invoke external signature service
+                byte[] cmsSignature = sign(externalSigning.getContent());
+
+                // Explanation of late external signing (off by default):
+                // If you want to add the signature in a separate step, then set an empty byte array
+                // and call signature.getByteRange() and remember the offset signature.getByteRange()[1]+1.
+                // you can write the ascii hex signature at a later time even if you don't have this
+                // PDDocument object anymore, with classic java file random access methods.
+                // If you can't remember the offset value from ByteRange because your context has changed,
+                // then open the file with PDFBox, find the field with findExistingSignature() or
+                // PDDocument.getLastSignatureDictionary() and get the ByteRange from there.
+                // Close the file and then write the signature as explained earlier in this comment.
+                if (isLateExternalSigning())
+                {
+                    // this saves the file with a 0 signature
+                    externalSigning.setSignature(new byte[0]);
+
+                    // remember the offset (add 1 because of "<")
+                    int offset = signature.getByteRange()[1] + 1;
+
+                    // now write the signature at the correct offset without any PDFBox methods
+                    try (RandomAccessFile raf = new RandomAccessFile(signedFile, "rw"))
+                    {
+                        raf.seek(offset);
+                        raf.write(Hex.getBytes(cmsSignature));
+                    }
+                }
+                else
+                {
+                    // set signature bytes received from the service and save the file
+                    externalSigning.setSignature(cmsSignature);
+                }
+            }
+            else
+            {
+                // write incremental (only for signing purpose)
+                doc.saveIncremental(fos);
+            }
+        }
+
+        // Do not close signatureOptions before saving, because some COSStream objects within
+        // are transferred to the signed document.
+        // Do not allow signatureOptions get out of scope before saving, because then the COSDocument
+        // in signature options might by closed by gc, which would close COSStream objects prematurely.
+        // See https://issues.apache.org/jira/browse/PDFBOX-3743
+        IOUtils.closeQuietly(signatureOptions);
+        IOUtils.closeQuietly(doc);
+    }
+
+    // Find an existing signature (assumed to be empty). You will usually not need this.
+    private PDSignature findExistingSignature(PDDocument doc, String sigFieldName)
+    {
+        PDSignature signature = null;
+        PDSignatureField signatureField;
+        PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm(null);
+        if (acroForm != null)
+        {
+            signatureField = (PDSignatureField) acroForm.getField(sigFieldName);
+            if (signatureField != null)
+            {
+                // retrieve signature dictionary
+                signature = signatureField.getSignature();
+                if (signature == null)
+                {
+                    signature = new PDSignature();
+                    // after solving PDFBOX-3524
+                    // signatureField.setValue(signature)
+                    // until then:
+                    signatureField.getCOSObject().setItem(COSName.V, signature);
+                }
+                else
+                {
+                    throw new IllegalStateException("The signature field " + sigFieldName + " is already signed.");
+                }
+            }
+        }
+        return signature;
+    }
+
+    /**
+     * Arguments are
+     * [0] key store
+     * [1] pin
+     * [2] document that will be signed
+     * [3] image of visible signature
+     *
+     * @param args
+     * @throws KeyStoreException
+     * @throws CertificateException
+     * @throws IOException
+     * @throws NoSuchAlgorithmException
+     * @throws UnrecoverableKeyException
+     */
+    public static void main(String[] args) throws KeyStoreException, CertificateException,
+            IOException, NoSuchAlgorithmException, UnrecoverableKeyException
+    {
+        // generate with
+        // keytool -storepass 123456 -storetype PKCS12 -keystore file.p12 -genkey -alias client -keyalg RSA
+        if (args.length < 4)
+        {
+            usage();
+            System.exit(1);
+        }
+
+        String tsaUrl = null;
+        // External signing is needed if you are using an external signing service, e.g. to sign
+        // several files at once.
+        boolean externalSig = false;
+        for (int i = 0; i < args.length; i++)
+        {
+            if ("-tsa".equals(args[i]))
+            {
+                i++;
+                if (i >= args.length)
+                {
+                    usage();
+                    System.exit(1);
+                }
+                tsaUrl = args[i];
+            }
+            if ("-e".equals(args[i]))
+            {
+                externalSig = true;
+            }
+        }
+
+        File ksFile = new File(args[0]);
+        KeyStore keystore = KeyStore.getInstance("PKCS12");
+        char[] pin = args[1].toCharArray();
+        keystore.load(new FileInputStream(ksFile), pin);
+
+        File documentFile = new File(args[2]);
+
+        CreateVisibleSignature signing = new CreateVisibleSignature(keystore, pin.clone());
+
+        File signedDocumentFile;
+        int page;
+        try (InputStream imageStream = new FileInputStream(args[3]))
+        {
+            String name = documentFile.getName();
+            String substring = name.substring(0, name.lastIndexOf('.'));
+            signedDocumentFile = new File(documentFile.getParent(), substring + "_signed.pdf");
+            // page is 1-based here
+            page = 1;
+            signing.setVisibleSignDesigner(args[2], 0, 0, -50, imageStream, page);
+        }
+        signing.setVisibleSignatureProperties("name", "location", "Security", 0, page, true);
+        signing.setExternalSigning(externalSig);
+        signing.signPDF(documentFile, signedDocumentFile, tsaUrl);
+    }
+
+    /**
+     * This will print the usage for this program.
+     */
+    private static void usage()
+    {
+        System.err.println("Usage: java " + CreateVisibleSignature.class.getName()
+                + " <pkcs12-keystore-file> <pin> <input-pdf> <sign-image>\n" + "" +
+                           "options:\n" +
+                           "  -tsa <url>    sign timestamp using the given TSA server\n"+
+                           "  -e            sign using external signature creation scenario");
+    }
+
+}

+ 532 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/CreateVisibleSignature2.java

@@ -0,0 +1,532 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.io.IOUtils;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.PDResources;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.common.PDStream;
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
+import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
+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.digitalsignature.SignatureOptions;
+import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
+import org.apache.pdfbox.pdmodel.interactive.form.PDField;
+import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
+import org.apache.pdfbox.util.Hex;
+import org.apache.pdfbox.util.Matrix;
+
+import javax.imageio.ImageIO;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Calendar;
+import java.util.List;
+
+/**
+ * This is a second example for visual signing a pdf. It doesn't use the "design pattern" influenced
+ * PDVisibleSignDesigner, and doesn't create its complex multilevel forms described in the Adobe
+ * document
+ * <a href="https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PPKAppearances.pdf">Digital
+ * Signature Appearances</a>, because this isn't required by the PDF specification. See the
+ * discussion in December 2017 in PDFBOX-3198.
+ *
+ * @author Vakhtang Koroghlishvili
+ * @author Tilman Hausherr
+ */
+public class CreateVisibleSignature2 extends CreateSignatureBase {
+    private SignatureOptions signatureOptions;
+    private boolean          lateExternalSigning = false;
+    private BufferedImage    image               = null;
+
+    /**
+     * Initialize the signature creator with a keystore (pkcs12) and pin that
+     * should be used for the signature.
+     *
+     * @param keystore is a pkcs12 keystore.
+     * @param pin      is the pin for the keystore / private key
+     * @throws KeyStoreException         if the keystore has not been initialized (loaded)
+     * @throws NoSuchAlgorithmException  if the algorithm for recovering the key cannot be found
+     * @throws UnrecoverableKeyException if the given password is wrong
+     * @throws CertificateException      if the certificate is not valid as signing time
+     * @throws IOException               if no certificate could be found
+     */
+    public CreateVisibleSignature2(KeyStore keystore, char[] pin)
+            throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException {
+        super(keystore, pin);
+    }
+
+    public BufferedImage getImage() {
+        return image;
+    }
+
+    public void setImage(BufferedImage image) {
+        this.image = image;
+    }
+
+    public boolean isLateExternalSigning() {
+        return lateExternalSigning;
+    }
+
+    /**
+     * Set late external signing. Enable this if you want to activate the demo code where the
+     * signature is kept and added in an extra step without using PDFBox methods. This is disabled
+     * by default.
+     *
+     * @param lateExternalSigning
+     */
+    public void setLateExternalSigning(boolean lateExternalSigning) {
+        this.lateExternalSigning = lateExternalSigning;
+    }
+
+    /**
+     * 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 humanRect  rectangle from a human viewpoint (coordinates start at top left)
+     * @param tsaUrl     optional TSA url
+     * @throws IOException
+     */
+    public void signPDF(File inputFile, File signedFile, Rectangle2D humanRect, String tsaUrl) throws IOException {
+        this.signPDF(inputFile, signedFile, humanRect, tsaUrl, null);
+    }
+
+    /**
+     * 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 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
+     * @throws IOException
+     */
+    public void signPDF(File inputFile, File signedFile, Rectangle2D humanRect, String tsaUrl, String signatureFieldName) throws IOException {
+        if (inputFile == null || !inputFile.exists()) {
+            throw new IOException("Document for signing does not exist");
+        }
+
+        setTsaUrl(tsaUrl);
+
+        // creating output document and prepare the IO streams.
+        try (FileOutputStream fos = new FileOutputStream(signedFile);
+             PDDocument doc = Loader.loadPDF(inputFile)) {
+            int accessPermissions = SigUtils.getMDPPermission(doc);
+            if (accessPermissions == 1) {
+                throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
+            }
+            // Note that PDFBox has a bug that visual signing on certified files with permission 2
+            // doesn't work properly, see PDFBOX-3699. As long as this issue is open, you may want to
+            // be careful with such files.
+
+            PDSignature signature = null;
+            PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm(null);
+            PDRectangle rect = null;
+
+            // sign a PDF with an existing empty signature, as created by the CreateEmptySignatureForm example.
+            if (acroForm != null) {
+                signature = findExistingSignature(acroForm, signatureFieldName);
+                if (signature != null) {
+                    rect = acroForm.getField(signatureFieldName).getWidgets().get(0).getRectangle();
+                }
+            }
+
+            if (signature == null) {
+                // create signature dictionary
+                signature = new PDSignature();
+            }
+
+            if (rect == null) {
+                rect = createSignatureRectangle(doc, humanRect);
+            }
+
+            // Optional: certify
+            // can be done only if version is at least 1.5 and if not already set
+            // doing this on a PDF/A-1b file fails validation by Adobe preflight (PDFBOX-3821)
+            // PDF/A-1b requires PDF version 1.4 max, so don't increase the version on such files.
+            if (doc.getVersion() >= 1.5f && accessPermissions == 0) {
+                SigUtils.setMDPPermission(doc, signature, 2);
+            }
+
+            if (acroForm != null && acroForm.getNeedAppearances()) {
+                // PDFBOX-3738 NeedAppearances true results in visible signature becoming invisible 
+                // with Adobe Reader
+                if (acroForm.getFields().isEmpty()) {
+                    // we can safely delete it if there are no fields
+                    acroForm.getCOSObject().removeItem(COSName.NEED_APPEARANCES);
+                    // note that if you've set MDP permissions, the removal of this item
+                    // may result in Adobe Reader claiming that the document has been changed.
+                    // and/or that field content won't be displayed properly.
+                    // ==> decide what you prefer and adjust your code accordingly.
+                } else {
+                    System.out.println("/NeedAppearances is set, signature may be ignored by Adobe Reader");
+                }
+            }
+
+            // default filter
+            signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
+
+            // subfilter for basic and PAdES Part 2 signatures
+            signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
+
+            signature.setName("Name");
+            signature.setLocation("Location");
+            signature.setReason("Reason");
+
+            // the signing date, needed for valid signature
+            signature.setSignDate(Calendar.getInstance());
+
+            // do not set SignatureInterface instance, if external signing used
+            SignatureInterface signatureInterface = isExternalSigning() ? null : this;
+
+            // register signature dictionary and sign interface
+            signatureOptions = new SignatureOptions();
+            signatureOptions.setVisualSignature(createVisualSignatureTemplate(doc, 0, rect, signature));
+            signatureOptions.setPage(doc.getNumberOfPages());
+            doc.addSignature(signature, signatureInterface, signatureOptions);
+
+            if (isExternalSigning()) {
+                ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
+                // invoke external signature service
+                byte[] cmsSignature = sign(externalSigning.getContent());
+
+                // Explanation of late external signing (off by default):
+                // If you want to add the signature in a separate step, then set an empty byte array
+                // and call signature.getByteRange() and remember the offset signature.getByteRange()[1]+1.
+                // you can write the ascii hex signature at a later time even if you don't have this
+                // PDDocument object anymore, with classic java file random access methods.
+                // If you can't remember the offset value from ByteRange because your context has changed,
+                // then open the file with PDFBox, find the field with findExistingSignature() or
+                // PDDocument.getLastSignatureDictionary() and get the ByteRange from there.
+                // Close the file and then write the signature as explained earlier in this comment.
+                if (isLateExternalSigning()) {
+                    // this saves the file with a 0 signature
+                    externalSigning.setSignature(new byte[0]);
+
+                    // remember the offset (add 1 because of "<")
+                    int offset = signature.getByteRange()[1] + 1;
+
+                    // now write the signature at the correct offset without any PDFBox methods
+                    try (RandomAccessFile raf = new RandomAccessFile(signedFile, "rw")) {
+                        raf.seek(offset);
+                        raf.write(Hex.getBytes(cmsSignature));
+                    }
+                } else {
+                    // set signature bytes received from the service and save the file
+                    externalSigning.setSignature(cmsSignature);
+                }
+            } else {
+                // write incremental (only for signing purpose)
+                doc.saveIncremental(fos);
+            }
+        }
+
+        // Do not close signatureOptions before saving, because some COSStream objects within
+        // are transferred to the signed document.
+        // Do not allow signatureOptions get out of scope before saving, because then the COSDocument
+        // in signature options might by closed by gc, which would close COSStream objects prematurely.
+        // See https://issues.apache.org/jira/browse/PDFBOX-3743
+        IOUtils.closeQuietly(signatureOptions);
+    }
+
+    private PDRectangle createSignatureRectangle(PDDocument doc, Rectangle2D humanRect) {
+        float x = (float) humanRect.getX();
+        float y = (float) humanRect.getY();
+        float width = (float) humanRect.getWidth();
+        float height = (float) humanRect.getHeight();
+        PDPage page = doc.getPage(0);
+        PDRectangle pageRect = page.getCropBox();
+        PDRectangle rect = new PDRectangle();
+        // signing should be at the same position regardless of page rotation.
+        switch (page.getRotation()) {
+            case 90:
+                rect.setLowerLeftY(x);
+                rect.setUpperRightY(x + width);
+                rect.setLowerLeftX(y);
+                rect.setUpperRightX(y + height);
+                break;
+            case 180:
+                rect.setUpperRightX(pageRect.getWidth() - x);
+                rect.setLowerLeftX(pageRect.getWidth() - x - width);
+                rect.setLowerLeftY(y);
+                rect.setUpperRightY(y + height);
+                break;
+            case 270:
+                rect.setLowerLeftY(pageRect.getHeight() - x - width);
+                rect.setUpperRightY(pageRect.getHeight() - x);
+                rect.setLowerLeftX(pageRect.getWidth() - y - height);
+                rect.setUpperRightX(pageRect.getWidth() - y);
+                break;
+            case 0:
+            default:
+                rect.setLowerLeftX(x);
+                rect.setUpperRightX(x + width);
+                rect.setLowerLeftY(pageRect.getHeight() - y - height);
+                rect.setUpperRightY(pageRect.getHeight() - y);
+                break;
+        }
+        return rect;
+    }
+
+    // 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) throws IOException {
+        try (PDDocument doc = new PDDocument()) {
+            PDPage page = new PDPage(srcDoc.getPage(pageNum).getMediaBox());
+            doc.addPage(page);
+            PDAcroForm acroForm = new PDAcroForm(doc);
+            doc.getDocumentCatalog().setAcroForm(acroForm);
+            PDSignatureField signatureField = new PDSignatureField(acroForm);
+            PDAnnotationWidget widget = signatureField.getWidgets().get(0);
+            List<PDField> acroFormFields = acroForm.getFields();
+            acroForm.setSignaturesExist(true);
+            acroForm.setAppendOnly(true);
+            acroForm.getCOSObject().setDirect(true);
+            acroFormFields.add(signatureField);
+
+            widget.setRectangle(rect);
+
+            // from PDVisualSigBuilder.createHolderForm()
+            PDStream stream = new PDStream(doc);
+            PDFormXObject form = new PDFormXObject(stream);
+            PDResources res = new PDResources();
+            form.setResources(res);
+            form.setFormType(1);
+            PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
+            float height = bbox.getHeight();
+            Matrix initialScale = null;
+            switch (srcDoc.getPage(pageNum).getRotation()) {
+                case 90:
+                    form.setMatrix(AffineTransform.getQuadrantRotateInstance(1));
+                    initialScale = Matrix
+                            .getScaleInstance(bbox.getWidth() / bbox.getHeight(), bbox.getHeight() / bbox.getWidth());
+                    height = bbox.getWidth();
+                    break;
+                case 180:
+                    form.setMatrix(AffineTransform.getQuadrantRotateInstance(2));
+                    break;
+                case 270:
+                    form.setMatrix(AffineTransform.getQuadrantRotateInstance(3));
+                    initialScale = Matrix
+                            .getScaleInstance(bbox.getWidth() / bbox.getHeight(), bbox.getHeight() / bbox.getWidth());
+                    height = bbox.getWidth();
+                    break;
+                case 0:
+                default:
+                    break;
+            }
+            form.setBBox(bbox);
+            PDFont font = PDType1Font.HELVETICA_BOLD;
+
+            // from PDVisualSigBuilder.createAppearanceDictionary()
+            PDAppearanceDictionary appearance = new PDAppearanceDictionary();
+            appearance.getCOSObject().setDirect(true);
+            PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
+            appearance.setNormalAppearance(appearanceStream);
+            widget.setAppearance(appearance);
+
+            try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
+                // for 90° and 270° scale ratio of width / height
+                // not really sure about this
+                // why does scale have no effect when done in the form matrix???
+                if (initialScale != null) {
+                    cs.transform(initialScale);
+                }
+
+                // show background (just for debugging, to see the rect size + position)
+//                cs.setNonStrokingColor(Color.yellow);
+//                cs.addRect(-5000, -5000, 10000, 10000);
+//                cs.fill();
+
+                if (image != null) {
+                    // show background image
+                    // save and restore graphics if the image is too large and needs to be scaled
+                    cs.saveGraphicsState();
+                    cs.transform(Matrix
+                            .getScaleInstance(rect.getHeight() / (float) image.getHeight(), rect
+                                    .getHeight() / (float) image.getHeight()));
+                    ByteArrayOutputStream out = new ByteArrayOutputStream();
+                    ImageIO.write(image, "png", out);
+                    PDImageXObject img = PDImageXObject.createFromByteArray(doc, out.toByteArray(), "sign.png");
+                    cs.drawImage(img, 0, 0);
+                    cs.restoreGraphicsState();
+                }
+
+                // show text
+                float fontSize = 10;
+                float leading = fontSize * 1.5f;
+//                cs.beginText();
+//                cs.setFont(font, fontSize);
+//                cs.setNonStrokingColor(Color.black);
+//                cs.newLineAtOffset(fontSize, height - leading);
+//                cs.setLeading(leading);
+//
+//                X509Certificate cert = (X509Certificate) getCertificateChain()[0];
+//
+//                // https://stackoverflow.com/questions/2914521/
+//                X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName());
+//                RDN cn = x500Name.getRDNs(BCStyle.CN)[0];
+//                String name = IETFUtils.valueToString(cn.getFirst().getValue());
+//
+//                // See https://stackoverflow.com/questions/12575990
+//                // for better date formatting
+//                String date = signature.getSignDate().getTime().toString();
+//                String reason = signature.getReason();
+//
+//                cs.showText("Signer: " + name);
+//                cs.newLine();
+//                cs.showText(date);
+//                cs.newLine();
+//                cs.showText("Reason: " + reason);
+//
+//                cs.endText();
+            }
+
+            // no need to set annotations and /P entry
+
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            doc.save(baos);
+            return new ByteArrayInputStream(baos.toByteArray());
+        }
+    }
+
+    // Find an existing signature (assumed to be empty). You will usually not need this.
+    private PDSignature findExistingSignature(PDAcroForm acroForm, String sigFieldName) {
+        PDSignature signature = null;
+        PDSignatureField signatureField;
+        if (acroForm != null) {
+            signatureField = (PDSignatureField) acroForm.getField(sigFieldName);
+            if (signatureField != null) {
+                // retrieve signature dictionary
+                signature = signatureField.getSignature();
+                if (signature == null) {
+                    signature = new PDSignature();
+                    // after solving PDFBOX-3524
+                    // signatureField.setValue(signature)
+                    // until then:
+                    signatureField.getCOSObject().setItem(COSName.V, signature);
+                } else {
+                    throw new IllegalStateException("The signature field " + sigFieldName + " is already signed.");
+                }
+            }
+        }
+        return signature;
+    }
+
+    /**
+     * Arguments are
+     * [0] key store
+     * [1] pin
+     * [2] document that will be signed
+     * [3] image of visible signature
+     *
+     * @param args
+     * @throws KeyStoreException
+     * @throws CertificateException
+     * @throws IOException
+     * @throws NoSuchAlgorithmException
+     * @throws UnrecoverableKeyException
+     */
+    public static void main(String[] args) throws KeyStoreException, CertificateException,
+            IOException, NoSuchAlgorithmException, UnrecoverableKeyException {
+        if (args.length < 3) {
+            usage();
+            System.exit(1);
+        }
+
+        String tsaUrl = null;
+        // External signing is needed if you are using an external signing service, e.g. to sign
+        // several files at once.
+        boolean externalSig = false;
+        for (int i = 0; i < args.length; i++) {
+            if ("-tsa".equals(args[i])) {
+                i++;
+                if (i >= args.length) {
+                    usage();
+                    System.exit(1);
+                }
+                tsaUrl = args[i];
+            }
+            if ("-e".equals(args[i])) {
+                externalSig = true;
+            }
+        }
+
+        File ksFile = new File(args[0]);
+        KeyStore keystore = KeyStore.getInstance("PKCS12");
+        char[] pin = args[1].toCharArray();
+        keystore.load(new FileInputStream(ksFile), pin);
+
+        File documentFile = new File(args[2]);
+
+        CreateVisibleSignature2 signing = new CreateVisibleSignature2(keystore, pin.clone());
+
+        if (args.length >= 4 && !"-tsa".equals(args[3])) {
+            signing.setImage(ImageIO.read(new File(args[3])));
+        }
+
+        File signedDocumentFile;
+        String name = documentFile.getName();
+        String substring = name.substring(0, name.lastIndexOf('.'));
+        signedDocumentFile = new File(documentFile.getParent(), substring + "_signed.pdf");
+
+        signing.setExternalSigning(externalSig);
+
+        // 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(100, 200, 150, 50);
+
+        signing.signPDF(documentFile, signedDocumentFile, humanRect, tsaUrl, "Signature1");
+    }
+
+    /**
+     * This will print the usage for this program.
+     */
+    private static void usage() {
+        System.err.println("Usage: java " + CreateVisibleSignature2.class.getName()
+                + " <pkcs12-keystore-file> <pin> <input-pdf> <sign-image>\n" + "" +
+                "options:\n" +
+                "  -tsa <url>    sign timestamp using the given TSA server\n" +
+                "  -e            sign using external signature creation scenario");
+
+        // generate pkcs12-keystore-file with
+        // keytool -storepass 123456 -storetype PKCS12 -keystore file.p12 -genkey -alias client -keyalg RSA
+    }
+
+}

+ 664 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/ShowSignature.java

@@ -0,0 +1,664 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert.CertificateVerificationException;
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert.CertificateVerifier;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.cos.*;
+import org.apache.pdfbox.io.IOUtils;
+import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
+import org.apache.pdfbox.pdfparser.PDFParser;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
+import org.apache.pdfbox.pdmodel.encryption.SecurityProvider;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.COSFilterInputStream;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
+import org.apache.pdfbox.util.Hex;
+import org.bouncycastle.asn1.cms.Attribute;
+import org.bouncycastle.asn1.cms.CMSAttributes;
+import org.bouncycastle.asn1.x509.Time;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaCertStore;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSProcessable;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.SignerInformation;
+import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.tsp.TSPException;
+import org.bouncycastle.tsp.TimeStampToken;
+import org.bouncycastle.tsp.TimeStampTokenInfo;
+import org.bouncycastle.util.CollectionStore;
+import org.bouncycastle.util.Selector;
+import org.bouncycastle.util.Store;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.*;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This will get the signature(s) from the document, do some verifications and
+ * show the signature(s) and the certificates. This is a complex topic - the
+ * code here is an example and not a production-ready solution.
+ *
+ * @author Ben Litchfield
+ */
+public final class ShowSignature
+{
+    private static final Log LOG = LogFactory.getLog(ShowSignature.class);
+
+    private final SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
+
+    private ShowSignature()
+    {
+    }
+
+    /**
+     * This is the entry point for the application.
+     *
+     * @param args The command-line arguments.
+     *
+     * @throws IOException If there is an error reading the file.
+     * @throws TSPException
+     * @throws org.apache.pdfbox.examples.signature.cert.CertificateVerificationException
+     * @throws GeneralSecurityException
+     */
+    public static void main(String[] args) throws IOException,
+                                                  TSPException,
+            CertificateVerificationException,
+                                                  GeneralSecurityException
+    {
+        // register BouncyCastle provider, needed for "exotic" algorithms
+        Security.addProvider(SecurityProvider.getProvider());
+
+        ShowSignature show = new ShowSignature();
+        show.showSignature( args );
+    }
+
+    private void showSignature(String[] args) throws IOException,
+                                                     GeneralSecurityException,
+                                                     TSPException,
+                                                     CertificateVerificationException
+    {
+        if( args.length != 2 )
+        {
+            usage();
+        }
+        else
+        {
+            String password = args[0];
+            File infile = new File(args[1]);
+            // use old-style document loading to disable leniency
+            // see also https://www.pdf-insecurity.org/
+            RandomAccessReadBufferedFile raFile = new RandomAccessReadBufferedFile(infile);
+            // If your files are not too large, you can also download the PDF into a byte array
+            // with IOUtils.toByteArray() and pass a RandomAccessBuffer() object to the
+            // PDFParser constructor.
+            PDFParser parser = new PDFParser(raFile, password);
+            try (PDDocument document = parser.parse(false))
+            {
+                for (PDSignature sig : document.getSignatureDictionaries())
+                {
+                    COSDictionary sigDict = sig.getCOSObject();
+                    byte[] contents = sig.getContents();
+
+                    // download the signed content
+                    // we're doing this as a stream, to be able to handle huge files                    
+                    try (FileInputStream fis = new FileInputStream(infile);
+                         InputStream signedContentAsStream = new COSFilterInputStream(fis, sig.getByteRange()))
+                    {
+                        System.out.println("Signature found");
+                        
+                        if (sig.getName() != null)
+                        {
+                            System.out.println("Name:     " + sig.getName());
+                        }
+                        if (sig.getSignDate() != null)
+                        {
+                            System.out.println("Modified: " + sdf.format(sig.getSignDate().getTime()));
+                        }
+                        String subFilter = sig.getSubFilter();
+                        if (subFilter != null)
+                        {
+                            switch (subFilter)
+                            {
+                                case "adbe.pkcs7.detached":
+                                case "ETSI.CAdES.detached":
+                                    verifyPKCS7(signedContentAsStream, contents, sig);
+                                    break;
+                                case "adbe.pkcs7.sha1":
+                                {
+                                    // example: PDFBOX-1452.pdf
+                                    CertificateFactory factory = CertificateFactory.getInstance("X.509");
+                                    ByteArrayInputStream certStream = new ByteArrayInputStream(contents);
+                                    Collection<? extends Certificate> certs = factory.generateCertificates(certStream);
+                                    System.out.println("certs=" + certs);
+                                    MessageDigest md = MessageDigest.getInstance("SHA1");
+                                    try (DigestInputStream dis = new DigestInputStream(signedContentAsStream, md))
+                                    {
+                                        while (dis.read() != -1)                                        
+                                        {
+                                            // do nothing
+                                        }
+                                    }
+                                    byte [] hash = md.digest();
+                                    verifyPKCS7(new ByteArrayInputStream(hash), contents, sig);
+                                    break;
+                                }
+                                case "adbe.x509.rsa_sha1":
+                                {
+                                    // example: PDFBOX-2693.pdf
+                                    COSString certString = (COSString) sigDict.getDictionaryObject(COSName.CERT);
+                                    //TODO this could also be an array.
+                                    if (certString == null)
+                                    {
+                                        System.err.println("The /Cert certificate string is missing in the signature dictionary");
+                                        return;
+                                    }
+                                    byte[] certData = certString.getBytes();
+                                    CertificateFactory factory = CertificateFactory.getInstance("X.509");
+                                    ByteArrayInputStream certStream = new ByteArrayInputStream(certData);
+                                    Collection<? extends Certificate> certs = factory.generateCertificates(certStream);
+                                    System.out.println("certs=" + certs);
+                                    
+                                    X509Certificate cert = (X509Certificate) certs.iterator().next();
+                                    
+                                    // to verify signature, see code at
+                                    // https://stackoverflow.com/questions/43383859/
+                                    
+                                    try
+                                    {
+                                        if (sig.getSignDate() != null)
+                                        {
+                                            cert.checkValidity(sig.getSignDate().getTime());
+                                            System.out.println("Certificate valid at signing time");
+                                        }
+                                        else
+                                        {
+                                            System.err.println("Certificate cannot be verified without signing time");
+                                        }
+                                    }
+                                    catch (CertificateExpiredException ex)
+                                    {
+                                        System.err.println("Certificate expired at signing time");
+                                    }
+                                    catch (CertificateNotYetValidException ex)
+                                    {
+                                        System.err.println("Certificate not yet valid at signing time");
+                                    }
+                                    if (CertificateVerifier.isSelfSigned(cert))
+                                    {
+                                        System.err.println("Certificate is self-signed, LOL!");
+                                    }
+                                    else
+                                    {
+                                        System.out.println("Certificate is not self-signed");
+                                        
+                                        if (sig.getSignDate() != null)
+                                        {
+                                            @SuppressWarnings("unchecked")
+                                                    Store<X509CertificateHolder> store = new JcaCertStore(certs);
+                                            SigUtils.verifyCertificateChain(store, cert, sig.getSignDate().getTime());
+                                        }
+                                    }
+                                    break;
+                                }
+                                case "ETSI.RFC3161":
+                                    // e.g. PDFBOX-1848, file_timestamped.pdf
+                                    verifyETSIdotRFC3161(signedContentAsStream, contents);
+                                    
+                                    // verifyPKCS7(hash, contents, sig) does not work
+                                    break;
+                                    
+                                default:
+                                    System.err.println("Unknown certificate type: " + subFilter);
+                                    break;
+                            }
+                        }
+                        else
+                        {
+                            throw new IOException("Missing subfilter for cert dictionary");
+                        }
+                        
+                        int[] byteRange = sig.getByteRange();
+                        if (byteRange.length != 4)
+                        {
+                            System.err.println("Signature byteRange must have 4 items");
+                        }
+                        else
+                        {
+                            long fileLen = infile.length();
+                            long rangeMax = byteRange[2] + (long) byteRange[3];
+                            // multiply content length with 2 (because it is in hex in the PDF) and add 2 for < and >
+                            int contentLen = contents.length * 2 + 2;
+                            if (fileLen != rangeMax || byteRange[0] != 0 || byteRange[1] + contentLen != byteRange[2])
+                            {
+                                // a false result doesn't necessarily mean that the PDF is a fake
+                                // see this answer why:
+                                // https://stackoverflow.com/a/48185913/535646
+                                System.out.println("Signature does not cover whole document");
+                            }
+                            else
+                            {
+                                System.out.println("Signature covers whole document");
+                            }
+                            checkContentValueWithFile(infile, byteRange, contents);
+                        }
+                    }
+                }
+                analyseDSS(document);
+            }
+            catch (CMSException | OperatorCreationException ex)
+            {
+                throw new IOException(ex);
+            }
+            System.out.println("Analyzed: " + args[1]);
+        }
+    }
+
+    private void checkContentValueWithFile(File file, int[] byteRange, byte[] contents) throws IOException
+    {
+        // https://stackoverflow.com/questions/55049270
+        // comment by mkl: check whether gap contains a hex value equal
+        // byte-by-byte to the Content value, to prevent attacker from using a literal string
+        // to allow extra space
+        try (RandomAccessReadBufferedFile raf = new RandomAccessReadBufferedFile(file))
+        {
+            raf.seek(byteRange[1]);
+            int c = raf.read();
+            if (c != '<')
+            {
+                System.err.println("'<' expected at offset " + byteRange[1] + ", but got " + (char) c);
+            }
+            byte[] contentFromFile = new byte[byteRange[2] - byteRange[1] - 2];
+            int contentLength = contentFromFile.length;
+            int contentBytesRead = raf.read(contentFromFile);
+            while (contentBytesRead > -1 && contentBytesRead < contentLength)
+            {
+                contentBytesRead += raf.read(contentFromFile,
+                        contentBytesRead,
+                        contentLength - contentBytesRead);
+            }
+            byte[] contentAsHex = Hex.getString(contents).getBytes(StandardCharsets.US_ASCII);
+            if (contentBytesRead != contentAsHex.length)
+            {
+                System.err.println("Raw content length from file is " +
+                        contentBytesRead +
+                        ", but internal content string in hex has length " +
+                        contentAsHex.length);
+            }
+            // Compare the two, we can't do byte comparison because of upper/lower case
+            // also check that it is really hex
+            for (int i = 0; i < contentBytesRead; ++i)
+            {
+                try
+                {
+                    if (Integer.parseInt(String.valueOf((char) contentFromFile[i]), 16) !=
+                        Integer.parseInt(String.valueOf((char) contentAsHex[i]), 16))
+                    {
+                        System.err.println("Possible manipulation at file offset " +
+                                (byteRange[1] + i + 1) + " in signature content");
+                        break;
+                    }
+                }
+                catch (NumberFormatException ex)
+                {
+                    System.err.println("Incorrect hex value");
+                    System.err.println("Possible manipulation at file offset " +
+                            (byteRange[1] + i + 1) + " in signature content");
+                    break;
+                }
+            }
+            c = raf.read();
+            if (c != '>')
+            {
+                System.err.println("'>' expected at offset " + byteRange[2] + ", but got " + (char) c);
+            }
+        }
+    }
+
+    /**
+     * Verify ETSI.RFC3161 TimeStampToken
+     *
+     * @param signedContentAsStream the byte sequence that has been signed
+     * @param contents the /Contents field as a COSString
+     * @throws CMSException
+     * @throws NoSuchAlgorithmException
+     * @throws IOException
+     * @throws TSPException
+     * @throws OperatorCreationException
+     * @throws CertificateVerificationException
+     * @throws CertificateException 
+     */
+    private void verifyETSIdotRFC3161(InputStream signedContentAsStream, byte[] contents)
+            throws CMSException, NoSuchAlgorithmException, IOException, TSPException,
+            OperatorCreationException, CertificateVerificationException, CertificateException
+    {
+        TimeStampToken timeStampToken = new TimeStampToken(new CMSSignedData(contents));
+        TimeStampTokenInfo timeStampInfo = timeStampToken.getTimeStampInfo();
+        System.out.println("Time stamp gen time: " + timeStampInfo.getGenTime());
+        if (timeStampInfo.getTsa() != null)
+        {
+            System.out.println("Time stamp tsa name: " + timeStampInfo.getTsa().getName());
+        }
+        
+        CertificateFactory factory = CertificateFactory.getInstance("X.509");
+        ByteArrayInputStream certStream = new ByteArrayInputStream(contents);
+        Collection<? extends Certificate> certs = factory.generateCertificates(certStream);
+        System.out.println("certs=" + certs);
+        
+        String hashAlgorithm = timeStampInfo.getMessageImprintAlgOID().getId();
+        // compare the hash of the signed content with the hash in the timestamp
+        MessageDigest md = MessageDigest.getInstance(hashAlgorithm);
+        try (DigestInputStream dis = new DigestInputStream(signedContentAsStream, md))
+        {
+            while (dis.read() != -1)
+            {
+                // do nothing
+            }
+        }
+        if (Arrays.equals(md.digest(),
+                timeStampInfo.getMessageImprintDigest()))
+        {
+            System.out.println("ETSI.RFC3161 timestamp signature verified");
+        }
+        else
+        {
+            System.err.println("ETSI.RFC3161 timestamp signature verification failed");
+        }
+
+        X509Certificate certFromTimeStamp = (X509Certificate) certs.iterator().next();
+        SigUtils.checkTimeStampCertificateUsage(certFromTimeStamp);
+        SigUtils.validateTimestampToken(timeStampToken);
+        SigUtils.verifyCertificateChain(timeStampToken.getCertificates(),
+                certFromTimeStamp,
+                timeStampInfo.getGenTime());
+    }
+
+    /**
+     * Verify a PKCS7 signature.
+     *
+     * @param signedContentAsStream the byte sequence that has been signed
+     * @param contents the /Contents field as a COSString
+     * @param sig the PDF signature (the /V dictionary)
+     * @throws CMSException
+     * @throws OperatorCreationException
+     * @throws GeneralSecurityException
+     * @throws CertificateVerificationException
+     */
+    private void verifyPKCS7(InputStream signedContentAsStream, byte[] contents, PDSignature sig)
+            throws CMSException, OperatorCreationException,
+                   CertificateVerificationException, GeneralSecurityException,
+                   TSPException, IOException
+    {
+        // inspiration:
+        // http://stackoverflow.com/a/26702631/535646
+        // http://stackoverflow.com/a/9261365/535646
+        CMSProcessable signedContent = new CMSProcessableInputStream(signedContentAsStream);
+        CMSSignedData signedData = new CMSSignedData(signedContent, contents);
+        Store<X509CertificateHolder> certificatesStore = signedData.getCertificates();
+        if (certificatesStore.getMatches(null).isEmpty())
+        {
+            throw new IOException("No certificates in signature");
+        }
+        Collection<SignerInformation> signers = signedData.getSignerInfos().getSigners();
+        if (signers.isEmpty())
+        {
+            throw new IOException("No signers in signature");
+        }
+        SignerInformation signerInformation = signers.iterator().next();
+        @SuppressWarnings("unchecked")
+        Collection<X509CertificateHolder> matches =
+                certificatesStore.getMatches((Selector<X509CertificateHolder>) signerInformation.getSID());
+        if (matches.isEmpty())
+        {
+            throw new IOException("Signer '" + signerInformation.getSID().getIssuer() + 
+                                  ", serial# " + signerInformation.getSID().getSerialNumber() + 
+                                  " does not match any certificates");
+        }
+        X509CertificateHolder certificateHolder = matches.iterator().next();
+        X509Certificate certFromSignedData = new JcaX509CertificateConverter().getCertificate(certificateHolder);
+        System.out.println("certFromSignedData: " + certFromSignedData);
+
+        SigUtils.checkCertificateUsage(certFromSignedData);
+        
+        // Embedded timestamp
+        TimeStampToken timeStampToken = SigUtils.extractTimeStampTokenFromSignerInformation(signerInformation);
+        if (timeStampToken != null)
+        {
+            // tested with QV_RCA1_RCA3_CPCPS_V4_11.pdf
+            // https://www.quovadisglobal.com/~/media/Files/Repository/QV_RCA1_RCA3_CPCPS_V4_11.ashx
+            // also 021496.pdf and 036351.pdf from digitalcorpora
+            SigUtils.validateTimestampToken(timeStampToken);
+            X509Certificate certFromTimeStamp = SigUtils.getCertificateFromTimeStampToken(timeStampToken);
+            // merge both stores using a set to remove duplicates
+            HashSet<X509CertificateHolder> certificateHolderSet = new HashSet<>();
+            certificateHolderSet.addAll(certificatesStore.getMatches(null));
+            certificateHolderSet.addAll(timeStampToken.getCertificates().getMatches(null));
+            SigUtils.verifyCertificateChain(new CollectionStore<>(certificateHolderSet),
+                    certFromTimeStamp,
+                    timeStampToken.getTimeStampInfo().getGenTime());
+            SigUtils.checkTimeStampCertificateUsage(certFromTimeStamp);
+
+            // compare the hash of the signature with the hash in the timestamp
+            byte[] tsMessageImprintDigest = timeStampToken.getTimeStampInfo().getMessageImprintDigest();
+            String hashAlgorithm = timeStampToken.getTimeStampInfo().getMessageImprintAlgOID().getId();
+            byte[] sigMessageImprintDigest = MessageDigest.getInstance(hashAlgorithm).digest(signerInformation.getSignature());
+            if (Arrays.equals(tsMessageImprintDigest, sigMessageImprintDigest))
+            {
+                System.out.println("timestamp signature verified");
+            }
+            else
+            {
+                System.err.println("timestamp signature verification failed");
+            }
+        }
+
+        try
+        {
+            if (sig.getSignDate() != null)
+            {
+                certFromSignedData.checkValidity(sig.getSignDate().getTime());
+                System.out.println("Certificate valid at signing time");
+            }
+            else
+            {
+                System.err.println("Certificate cannot be verified without signing time");
+            }
+        }
+        catch (CertificateExpiredException ex)
+        {
+            System.err.println("Certificate expired at signing time");
+        }
+        catch (CertificateNotYetValidException ex)
+        {
+            System.err.println("Certificate not yet valid at signing time");
+        }
+
+        // usually not available
+        if (signerInformation.getSignedAttributes() != null)
+        {
+            // From SignedMailValidator.getSignatureTime()
+            Attribute signingTime = signerInformation.getSignedAttributes().get(CMSAttributes.signingTime);
+            if (signingTime != null)
+            {
+                Time timeInstance = Time.getInstance(signingTime.getAttrValues().getObjectAt(0));
+                try
+                {
+                    certFromSignedData.checkValidity(timeInstance.getDate());
+                    System.out.println("Certificate valid at signing time: " + timeInstance.getDate());
+                }
+                catch (CertificateExpiredException ex)
+                {
+                    System.err.println("Certificate expired at signing time");
+                }
+                catch (CertificateNotYetValidException ex)
+                {
+                    System.err.println("Certificate not yet valid at signing time");
+                }
+            }
+        }
+
+        if (signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().
+                setProvider(SecurityProvider.getProvider()).build(certFromSignedData)))
+        {
+            System.out.println("Signature verified");
+        }
+        else
+        {
+            System.out.println("Signature verification failed");
+        }
+
+        if (CertificateVerifier.isSelfSigned(certFromSignedData))
+        {
+            System.err.println("Certificate is self-signed, LOL!");
+        }
+        else
+        {
+            System.out.println("Certificate is not self-signed");
+
+            if (sig.getSignDate() != null)
+            {
+                SigUtils.verifyCertificateChain(certificatesStore, certFromSignedData, sig.getSignDate().getTime());
+            }
+            else
+            {
+                System.err.println("Certificate cannot be verified without signing time");
+            }
+        }
+    }
+
+    // for later use: get all root certificates. Will be used to check
+    // whether we trust the root in the certificate chain.
+    private Set<X509Certificate> getRootCertificates()
+            throws GeneralSecurityException, IOException
+    {
+        Set<X509Certificate> rootCertificates = new HashSet<>();
+
+        // https://stackoverflow.com/questions/3508050/
+        String filename = System.getProperty("java.home") + "/lib/security/cacerts";
+        KeyStore keystore;
+        try (FileInputStream is = new FileInputStream(filename))
+        {
+            keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+            keystore.load(is, null);
+        }
+        PKIXParameters params = new PKIXParameters(keystore);
+        for (TrustAnchor trustAnchor : params.getTrustAnchors())
+        {
+            rootCertificates.add(trustAnchor.getTrustedCert());
+        }
+
+        // https://www.oracle.com/technetwork/articles/javase/security-137537.html
+        try
+        {
+            keystore = KeyStore.getInstance("Windows-ROOT");
+            keystore.load(null, null);
+            params = new PKIXParameters(keystore);
+            for (TrustAnchor trustAnchor : params.getTrustAnchors())
+            {
+                rootCertificates.add(trustAnchor.getTrustedCert());
+            }
+        }
+        catch (InvalidAlgorithmParameterException | KeyStoreException ex)
+        {
+            // empty or not windows
+        }
+
+        return rootCertificates;
+    }
+
+    /**
+     * Analyzes the DSS-Dictionary (Document Security Store) of the document. Which is used for signature validation.
+     * The DSS is defined in PAdES Part 4 - Long Term Validation.
+     * 
+     * @param document PDDocument, to get the DSS from
+     */
+    private void analyseDSS(PDDocument document) throws IOException
+    {
+        PDDocumentCatalog catalog = document.getDocumentCatalog();
+        COSBase dssElement = catalog.getCOSObject().getDictionaryObject("DSS");
+
+        if (dssElement instanceof COSDictionary)
+        {
+            COSDictionary dss = (COSDictionary) dssElement;
+            System.out.println("DSS Dictionary: " + dss);
+            COSBase certsElement = dss.getDictionaryObject("Certs");
+            if (certsElement instanceof COSArray)
+            {
+                printStreamsFromArray((COSArray) certsElement, "Cert");
+            }
+            COSBase ocspsElement = dss.getDictionaryObject("OCSPs");
+            if (ocspsElement instanceof COSArray)
+            {
+                printStreamsFromArray((COSArray) ocspsElement, "Ocsp");
+            }
+            COSBase crlElement = dss.getDictionaryObject("CRLs");
+            if (crlElement instanceof COSArray)
+            {
+                printStreamsFromArray((COSArray) crlElement, "CRL");
+            }
+            // TODO: go through VRIs (which indirectly point to the DSS-Data)
+        }
+    }
+
+    /**
+     * Go through the elements of a COSArray containing each an COSStream to print in Hex.
+     * 
+     * @param elements COSArray of elements containing a COS Stream
+     * @param description to append on Print
+     * @throws IOException
+     */
+    private void printStreamsFromArray(COSArray elements, String description) throws IOException
+    {
+        for (COSBase baseElem : elements)
+        {
+            COSObject streamObj = (COSObject) baseElem;
+            if (streamObj.getObject() instanceof COSStream)
+            {
+                COSStream cosStream = (COSStream) streamObj.getObject();
+                try (InputStream is = cosStream.createInputStream())
+                {
+                    byte[] streamBytes = IOUtils.toByteArray(is);
+                    System.out.println(description + " (" + elements.indexOf(streamObj) + "): "
+                        + Hex.getString(streamBytes));
+                }
+            }
+        }
+    }
+
+    /**
+     * This will print a usage message.
+     */
+    private static void usage()
+    {
+        System.err.println( "usage: java " + ShowSignature.class.getName() +
+                            " <password (usually empty)> <inputfile>" );
+        // The password is for encrypted files and has nothing to do with the signature.
+        // (A PDF can be both encrypted and signed)
+    }
+}

+ 383 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/SigUtils.java

@@ -0,0 +1,383 @@
+/*
+ * Copyright 2017 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert.CertificateVerificationException;
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert.CertificateVerifier;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+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.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.encryption.SecurityProvider;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
+import org.bouncycastle.asn1.ASN1Object;
+import org.bouncycastle.asn1.cms.Attribute;
+import org.bouncycastle.asn1.cms.AttributeTable;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.SignerInformation;
+import org.bouncycastle.cms.SignerInformationVerifier;
+import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.tsp.TSPException;
+import org.bouncycastle.tsp.TimeStampToken;
+import org.bouncycastle.util.Selector;
+import org.bouncycastle.util.Store;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.*;
+
+/**
+ * Utility class for the signature / timestamp examples.
+ * 
+ * @author Tilman Hausherr
+ */
+public class SigUtils
+{
+    private static final Log LOG = LogFactory.getLog(SigUtils.class);
+
+    private SigUtils()
+    {
+    }
+
+    /**
+     * Get the access permissions granted for this document in the DocMDP transform parameters
+     * dictionary. Details are described in the table "Entries in the DocMDP transform parameters
+     * dictionary" in the PDF specification.
+     *
+     * @param doc document.
+     * @return the permission value. 0 means no DocMDP transform parameters dictionary exists. Other
+     * return values are 1, 2 or 3. 2 is also returned if the DocMDP transform parameters dictionary
+     * is found but did not contain a /P entry, or if the value is outside the valid range.
+     */
+    public static int getMDPPermission(PDDocument doc)
+    {
+        COSDictionary permsDict = doc.getDocumentCatalog().getCOSObject()
+                .getCOSDictionary(COSName.PERMS);
+        if (permsDict != null)
+        {
+            COSDictionary signatureDict = permsDict.getCOSDictionary(COSName.DOCMDP);
+            if (signatureDict != null)
+            {
+                COSArray refArray = signatureDict.getCOSArray(COSName.REFERENCE);
+                if (refArray instanceof COSArray)
+                {
+                    for (int i = 0; i < refArray.size(); ++i)
+                    {
+                        COSBase base = refArray.getObject(i);
+                        if (base instanceof COSDictionary)
+                        {
+                            COSDictionary sigRefDict = (COSDictionary) base;
+                            if (COSName.DOCMDP.equals(sigRefDict.getDictionaryObject(COSName.TRANSFORM_METHOD)))
+                            {
+                                base = sigRefDict.getDictionaryObject(COSName.TRANSFORM_PARAMS);
+                                if (base instanceof COSDictionary)
+                                {
+                                    COSDictionary transformDict = (COSDictionary) base;
+                                    int accessPermissions = transformDict.getInt(COSName.P, 2);
+                                    if (accessPermissions < 1 || accessPermissions > 3)
+                                    {
+                                        accessPermissions = 2;
+                                    }
+                                    return accessPermissions;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Set the "modification detection and prevention" permissions granted for this document in the
+     * DocMDP transform parameters dictionary. Details are described in the table "Entries in the
+     * DocMDP transform parameters dictionary" in the PDF specification.
+     *
+     * @param doc The document.
+     * @param signature The signature object.
+     * @param accessPermissions The permission value (1, 2 or 3).
+     *
+     * @throws IOException if a signature exists.
+     */
+    public static void setMDPPermission(PDDocument doc, PDSignature signature, int accessPermissions)
+            throws IOException
+    {
+        for (PDSignature sig : doc.getSignatureDictionaries())
+        {
+            // "Approval signatures shall follow the certification signature if one is present"
+            // thus we don't care about timestamp signatures
+            if (COSName.DOC_TIME_STAMP.equals(sig.getCOSObject().getItem(COSName.TYPE)))
+            {
+                continue;
+            }
+            if (sig.getCOSObject().containsKey(COSName.CONTENTS))
+            {
+                throw new IOException("DocMDP transform method not allowed if an approval signature exists");
+            }
+        }
+
+        COSDictionary sigDict = signature.getCOSObject();
+
+        // DocMDP specific stuff
+        COSDictionary transformParameters = new COSDictionary();
+        transformParameters.setItem(COSName.TYPE, COSName.TRANSFORM_PARAMS);
+        transformParameters.setInt(COSName.P, accessPermissions);
+        transformParameters.setName(COSName.V, "1.2");
+        transformParameters.setNeedToBeUpdated(true);
+
+        COSDictionary referenceDict = new COSDictionary();
+        referenceDict.setItem(COSName.TYPE, COSName.SIG_REF);
+        referenceDict.setItem(COSName.TRANSFORM_METHOD, COSName.DOCMDP);
+        referenceDict.setItem(COSName.DIGEST_METHOD, COSName.getPDFName("SHA1"));
+        referenceDict.setItem(COSName.TRANSFORM_PARAMS, transformParameters);
+        referenceDict.setNeedToBeUpdated(true);
+
+        COSArray referenceArray = new COSArray();
+        referenceArray.add(referenceDict);
+        sigDict.setItem(COSName.REFERENCE, referenceArray);
+        referenceArray.setNeedToBeUpdated(true);
+
+        // Catalog
+        COSDictionary catalogDict = doc.getDocumentCatalog().getCOSObject();
+        COSDictionary permsDict = new COSDictionary();
+        catalogDict.setItem(COSName.PERMS, permsDict);
+        permsDict.setItem(COSName.DOCMDP, signature);
+        catalogDict.setNeedToBeUpdated(true);
+        permsDict.setNeedToBeUpdated(true);
+    }
+
+    /**
+     * Log if the certificate is not valid for signature usage. Doing this
+     * anyway results in Adobe Reader failing to validate the PDF.
+     *
+     * @param x509Certificate 
+     * @throws CertificateParsingException
+     */
+    public static void checkCertificateUsage(X509Certificate x509Certificate)
+            throws CertificateParsingException
+    {
+        // Check whether signer certificate is "valid for usage"
+        // https://stackoverflow.com/a/52765021/535646
+        // https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/changes.html#id1
+        boolean[] keyUsage = x509Certificate.getKeyUsage();
+        if (keyUsage != null && !keyUsage[0] && !keyUsage[1])
+        {
+            // (unclear what "signTransaction" is)
+            // https://tools.ietf.org/html/rfc5280#section-4.2.1.3
+            LOG.error("Certificate key usage does not include " +
+                    "digitalSignature nor nonRepudiation");
+        }
+        List<String> extendedKeyUsage = x509Certificate.getExtendedKeyUsage();
+        if (extendedKeyUsage != null &&
+            !extendedKeyUsage.contains(KeyPurposeId.id_kp_emailProtection.toString()) &&
+            !extendedKeyUsage.contains(KeyPurposeId.id_kp_codeSigning.toString()) &&
+            !extendedKeyUsage.contains(KeyPurposeId.anyExtendedKeyUsage.toString()) &&
+            !extendedKeyUsage.contains("1.2.840.113583.1.1.5") &&
+            // not mentioned in Adobe document, but tolerated in practice
+            !extendedKeyUsage.contains("1.3.6.1.4.1.311.10.3.12"))
+        {
+            LOG.error("Certificate extended key usage does not include " +
+                    "emailProtection, nor codeSigning, nor anyExtendedKeyUsage, " +
+                    "nor 'Adobe Authentic Documents Trust'");
+        }
+    }
+
+    /**
+     * Log if the certificate is not valid for timestamping.
+     *
+     * @param x509Certificate
+     * @throws CertificateParsingException
+     */
+    public static void checkTimeStampCertificateUsage(X509Certificate x509Certificate)
+            throws CertificateParsingException
+    {
+        List<String> extendedKeyUsage = x509Certificate.getExtendedKeyUsage();
+        // https://tools.ietf.org/html/rfc5280#section-4.2.1.12
+        if (extendedKeyUsage != null &&
+            !extendedKeyUsage.contains(KeyPurposeId.id_kp_timeStamping.toString()))
+        {
+            LOG.error("Certificate extended key usage does not include timeStamping");
+        }
+    }
+
+    /**
+     * Log if the certificate is not valid for responding.
+     *
+     * @param x509Certificate
+     * @throws CertificateParsingException
+     */
+    public static void checkResponderCertificateUsage(X509Certificate x509Certificate)
+            throws CertificateParsingException
+    {
+        List<String> extendedKeyUsage = x509Certificate.getExtendedKeyUsage();
+        // https://tools.ietf.org/html/rfc5280#section-4.2.1.12
+        if (extendedKeyUsage != null &&
+            !extendedKeyUsage.contains(KeyPurposeId.id_kp_OCSPSigning.toString()))
+        {
+            LOG.error("Certificate extended key usage does not include OCSP responding");
+        }
+    }
+
+    /**
+     * Gets the last relevant signature in the document, i.e. the one with the highest offset.
+     * 
+     * @param document to get its last signature
+     * @return last signature or null when none found
+     */
+    public static PDSignature getLastRelevantSignature(PDDocument document)
+    {
+        Comparator<PDSignature> comparatorByOffset =
+                Comparator.comparing(sig -> sig.getByteRange()[1]);
+
+        // we can't use getLastSignatureDictionary() because this will fail (see PDFBOX-3978) 
+        // if a signature is assigned to a pre-defined empty signature field that isn't the last.
+        // we get the last in time by looking at the offset in the PDF file.
+        Optional<PDSignature> optLastSignature =
+                document.getSignatureDictionaries().stream().
+                sorted(comparatorByOffset.reversed()).
+                findFirst();
+        if (optLastSignature.isPresent())
+        {
+            PDSignature lastSignature = optLastSignature.get();
+            COSBase type = lastSignature.getCOSObject().getItem(COSName.TYPE);
+            if (type == null || COSName.SIG.equals(type) || COSName.DOC_TIME_STAMP.equals(type))
+            {
+                return lastSignature;
+            }
+        }
+        return null;
+    }
+
+    public static TimeStampToken extractTimeStampTokenFromSignerInformation(SignerInformation signerInformation)
+            throws CMSException, IOException, TSPException
+    {
+        if (signerInformation.getUnsignedAttributes() == null)
+        {
+            return null;
+        }
+        AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
+        // https://stackoverflow.com/questions/1647759/how-to-validate-if-a-signed-jar-contains-a-timestamp
+        Attribute attribute = unsignedAttributes.get(
+                PKCSObjectIdentifiers.id_aa_signatureTimeStampToken);
+        if (attribute == null)
+        {
+            return null;
+        }
+        ASN1Object obj = (ASN1Object) attribute.getAttrValues().getObjectAt(0);
+        CMSSignedData signedTSTData = new CMSSignedData(obj.getEncoded());
+        return new TimeStampToken(signedTSTData);
+    }
+
+    public static void validateTimestampToken(TimeStampToken timeStampToken)
+            throws TSPException, CertificateException, OperatorCreationException, IOException
+    {
+        // https://stackoverflow.com/questions/42114742/
+        @SuppressWarnings("unchecked") // TimeStampToken.getSID() is untyped
+        Collection<X509CertificateHolder> tstMatches =
+                timeStampToken.getCertificates().getMatches((Selector<X509CertificateHolder>) timeStampToken.getSID());
+        X509CertificateHolder certificateHolder = tstMatches.iterator().next();
+        SignerInformationVerifier siv = 
+                new JcaSimpleSignerInfoVerifierBuilder().setProvider(SecurityProvider.getProvider()).build(certificateHolder);
+        timeStampToken.validate(siv);
+    }
+
+    /**
+     * Verify the certificate chain up to the root, including OCSP or CRL. However this does not
+     * test whether the root certificate is in a trusted list.<br><br>
+     * Please post bad PDF files that succeed and good PDF files that fail in
+     * <a href="https://issues.apache.org/jira/browse/PDFBOX-3017">PDFBOX-3017</a>.
+     *
+     * @param certificatesStore
+     * @param certFromSignedData
+     * @param signDate
+     * @throws CertificateVerificationException
+     * @throws CertificateException
+     */
+    public static void verifyCertificateChain(Store<X509CertificateHolder> certificatesStore,
+            X509Certificate certFromSignedData, Date signDate)
+            throws CertificateVerificationException, CertificateException
+    {
+        Collection<X509CertificateHolder> certificateHolders = certificatesStore.getMatches(null);
+        Set<X509Certificate> additionalCerts = new HashSet<>();
+        JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
+        for (X509CertificateHolder certHolder : certificateHolders)
+        {
+            X509Certificate certificate = certificateConverter.getCertificate(certHolder);
+            if (!certificate.equals(certFromSignedData))
+            {
+                additionalCerts.add(certificate);
+            }
+        }
+        CertificateVerifier.verifyCertificate(certFromSignedData, additionalCerts, true, signDate);
+        //TODO check whether the root certificate is in our trusted list.
+        // For the EU, get a list here:
+        // https://ec.europa.eu/digital-single-market/en/eu-trusted-lists-trust-service-providers
+        // ( getRootCertificates() is not helpful because these are SSL certificates)
+    }
+
+    /**
+     * Get certificate of a TSA.
+     * 
+     * @param tsaUrl URL
+     * @return the X.509 certificate.
+     *
+     * @throws GeneralSecurityException
+     * @throws IOException 
+     */
+    public static X509Certificate getTsaCertificate(String tsaUrl)
+            throws GeneralSecurityException, IOException
+    {
+        MessageDigest digest = MessageDigest.getInstance("SHA-256");
+        TSAClient tsaClient = new TSAClient(new URL(tsaUrl), null, null, digest);
+        InputStream emptyStream = new ByteArrayInputStream(new byte[0]);
+        TimeStampToken timeStampToken = tsaClient.getTimeStampToken(emptyStream);
+        return getCertificateFromTimeStampToken(timeStampToken);
+    }
+
+    /**
+     * Extract X.509 certificate from a timestamp
+     * @param timeStampToken
+     * @return the X.509 certificate.
+     * @throws CertificateException 
+     */
+    public static X509Certificate getCertificateFromTimeStampToken(TimeStampToken timeStampToken)
+            throws CertificateException
+    {
+        @SuppressWarnings("unchecked") // TimeStampToken.getSID() is untyped
+        Collection<X509CertificateHolder> tstMatches =
+                timeStampToken.getCertificates().getMatches(timeStampToken.getSID());
+        X509CertificateHolder tstCertHolder = tstMatches.iterator().next();
+        return new JcaX509CertificateConverter().getCertificate(tstCertHolder);
+    }
+}

+ 172 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/TSAClient.java

@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.io.IOUtils;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
+import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder;
+import org.bouncycastle.tsp.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Random;
+
+/**
+ * Time Stamping Authority (TSA) Client [RFC 3161].
+ * @author Vakhtang Koroghlishvili
+ * @author John Hewson
+ */
+public class TSAClient
+{
+    private static final Log LOG = LogFactory.getLog(TSAClient.class);
+
+    private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER =
+            new DefaultDigestAlgorithmIdentifierFinder();
+
+    private final URL url;
+    private final String username;
+    private final String password;
+    private final MessageDigest digest;
+
+    // SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
+    private static final Random RANDOM = new SecureRandom();
+
+    /**
+     *
+     * @param url the URL of the TSA service
+     * @param username user name of TSA
+     * @param password password of TSA
+     * @param digest the message digest to use
+     */
+    public TSAClient(URL url, String username, String password, MessageDigest digest)
+    {
+        this.url = url;
+        this.username = username;
+        this.password = password;
+        this.digest = digest;
+    }
+
+    /**
+     *
+     * @param content
+     * @return the time stamp token
+     * @throws IOException if there was an error with the connection or data from the TSA server,
+     *                     or if the time stamp response could not be validated
+     */
+    public TimeStampToken getTimeStampToken(InputStream content) throws IOException
+    {
+        digest.reset();
+        DigestInputStream dis = new DigestInputStream(content, digest);
+        while (dis.read() != -1)
+        {
+            // do nothing
+        }
+        byte[] hash = digest.digest();
+
+        // 32-bit cryptographic nonce
+        int nonce = RANDOM.nextInt();
+
+        // generate TSA request
+        TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
+        tsaGenerator.setCertReq(true);
+        ASN1ObjectIdentifier oid = ALGORITHM_OID_FINDER.find(digest.getAlgorithm()).getAlgorithm();
+        TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
+
+        // get TSA response
+        byte[] tsaResponse = getTSAResponse(request.getEncoded());
+
+        TimeStampResponse response;
+        try
+        {
+            response = new TimeStampResponse(tsaResponse);
+            response.validate(request);
+        }
+        catch (TSPException e)
+        {
+            throw new IOException(e);
+        }
+
+        TimeStampToken timeStampToken = response.getTimeStampToken();
+        if (timeStampToken == null)
+        {
+            // https://www.ietf.org/rfc/rfc3161.html#section-2.4.2
+            throw new IOException("Response from " + url +
+                    " does not have a time stamp token, status: " + response.getStatus() +
+                    " (" + response.getStatusString() + ")");
+        }
+
+        return timeStampToken;
+    }
+
+    // gets response data for the given encoded TimeStampRequest data
+    // throws IOException if a connection to the TSA cannot be established
+    private byte[] getTSAResponse(byte[] request) throws IOException
+    {
+        LOG.debug("Opening connection to TSA server");
+
+        // todo: support proxy servers
+        URLConnection connection = url.openConnection();
+        connection.setDoOutput(true);
+        connection.setDoInput(true);
+        connection.setRequestProperty("Content-Type", "application/timestamp-query");
+
+        LOG.debug("Established connection to TSA server");
+
+        if (username != null && password != null && !username.isEmpty() && !password.isEmpty())
+        {
+            connection.setRequestProperty(username, password);
+        }
+
+        // read response
+        try (OutputStream output = connection.getOutputStream())
+        {
+            output.write(request);
+        }
+        catch (IOException ex)
+        {
+            LOG.error("Exception when writing to " + this.url, ex);
+            throw ex;
+        }
+
+        LOG.debug("Waiting for response from TSA server");
+
+        byte[] response;
+        try (InputStream input = connection.getInputStream())
+        {
+            response = IOUtils.toByteArray(input);
+        }
+        catch (IOException ex)
+        {
+            LOG.error("Exception when reading from " + this.url, ex);
+            throw ex;
+        }
+
+        LOG.debug("Received response from TSA server");
+
+        return response;
+    }
+}

+ 135 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/ValidationTimeStamp.java

@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature;
+
+import org.bouncycastle.asn1.*;
+import org.bouncycastle.asn1.cms.Attribute;
+import org.bouncycastle.asn1.cms.AttributeTable;
+import org.bouncycastle.asn1.cms.Attributes;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.SignerInformation;
+import org.bouncycastle.cms.SignerInformationStore;
+import org.bouncycastle.tsp.TimeStampToken;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class wraps the TSAClient and the work that has to be done with it. Like Adding Signed
+ * TimeStamps to a signature, or creating a CMS timestamp attribute (with a signed timestamp)
+ *
+ * @author Others
+ * @author Alexis Suter
+ */
+public class ValidationTimeStamp
+{
+    private TSAClient tsaClient;
+
+    /**
+     * @param tsaUrl The url where TS-Request will be done.
+     * @throws NoSuchAlgorithmException
+     * @throws MalformedURLException
+     */
+    public ValidationTimeStamp(String tsaUrl) throws NoSuchAlgorithmException, MalformedURLException
+    {
+        if (tsaUrl != null)
+        {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            this.tsaClient = new TSAClient(new URL(tsaUrl), null, null, digest);
+        }
+    }
+
+    /**
+     * Creates a signed timestamp token by the given input stream.
+     * 
+     * @param content InputStream of the content to sign
+     * @return the byte[] of the timestamp token
+     * @throws IOException
+     */
+    public byte[] getTimeStampToken(InputStream content) throws IOException
+    {
+        TimeStampToken timeStampToken = tsaClient.getTimeStampToken(content);
+        return timeStampToken.getEncoded();
+    }
+
+    /**
+     * Extend cms signed data with TimeStamp first or to all signers
+     *
+     * @param signedData Generated CMS signed data
+     * @return CMSSignedData Extended CMS signed data
+     * @throws IOException
+     */
+    public CMSSignedData addSignedTimeStamp(CMSSignedData signedData)
+            throws IOException
+    {
+        SignerInformationStore signerStore = signedData.getSignerInfos();
+        List<SignerInformation> newSigners = new ArrayList<>();
+
+        for (SignerInformation signer : signerStore.getSigners())
+        {
+            // This adds a timestamp to every signer (into his unsigned attributes) in the signature.
+            newSigners.add(signTimeStamp(signer));
+        }
+
+        // Because new SignerInformation is created, new SignerInfoStore has to be created 
+        // and also be replaced in signedData. Which creates a new signedData object.
+        return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners));
+    }
+
+    /**
+     * Extend CMS Signer Information with the TimeStampToken into the unsigned Attributes.
+     *
+     * @param signer information about signer
+     * @return information about SignerInformation
+     * @throws IOException
+     */
+    private SignerInformation signTimeStamp(SignerInformation signer)
+            throws IOException
+    {
+        AttributeTable unsignedAttributes = signer.getUnsignedAttributes();
+
+        ASN1EncodableVector vector = new ASN1EncodableVector();
+        if (unsignedAttributes != null)
+        {
+            vector = unsignedAttributes.toASN1EncodableVector();
+        }
+
+        TimeStampToken timeStampToken = tsaClient.getTimeStampToken(
+                new ByteArrayInputStream(signer.getSignature()));
+        byte[] token = timeStampToken.getEncoded();
+        ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;
+        ASN1Encodable signatureTimeStamp = new Attribute(oid,
+                new DERSet(ASN1Primitive.fromByteArray(token)));
+
+        vector.add(signatureTimeStamp);
+        Attributes signedAttributes = new Attributes(vector);
+
+        // There is no other way changing the unsigned attributes of the signer information.
+        // result is never null, new SignerInformation always returned, 
+        // see source code of replaceUnsignedAttributes
+        return SignerInformation.replaceUnsignedAttributes(signer, new AttributeTable(signedAttributes));
+    }
+}

+ 341 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/CRLVerifier.java

@@ -0,0 +1,341 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.pdmodel.encryption.SecurityProvider;
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1OctetString;
+import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.DERIA5String;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.*;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.cert.*;
+import java.util.*;
+
+/**
+ * Copied from Apache CXF 2.4.9, initial version:
+ * https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.9/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/
+ * 
+ */
+public final class CRLVerifier
+{
+    private static final Log LOG = LogFactory.getLog(CRLVerifier.class);
+
+    private CRLVerifier()
+    {
+    }
+
+    /**
+     * Extracts the CRL distribution points from the certificate (if available)
+     * and checks the certificate revocation status against the CRLs coming from
+     * the distribution points. Supports HTTP, HTTPS, FTP and LDAP based URLs.
+     *
+     * @param cert the certificate to be checked for revocation
+     * @param signDate the date when the signing took place
+     * @param additionalCerts set of trusted root CA certificates that will be
+     * used as "trust anchors" and intermediate CA certificates that will be
+     * used as part of the certification chain.
+     * @throws CertificateVerificationException if the certificate could not be verified
+     * @throws RevokedCertificateException if the certificate is revoked
+     */
+    @SuppressWarnings({"squid:S1141"}) // nested exception needed to try several distribution points
+    public static void verifyCertificateCRLs(X509Certificate cert, Date signDate,
+            Set<X509Certificate> additionalCerts)
+            throws CertificateVerificationException, RevokedCertificateException
+    {
+        try
+        {
+            Date now = Calendar.getInstance().getTime();
+            Exception firstException = null;
+            List<String> crlDistributionPointsURLs = getCrlDistributionPoints(cert);
+            for (String crlDistributionPointsURL : crlDistributionPointsURLs)
+            {
+                LOG.info("Checking distribution point URL: " + crlDistributionPointsURL);
+
+                X509CRL crl;
+                try
+                {
+                    crl = downloadCRL(crlDistributionPointsURL);
+                }
+                catch (IOException | GeneralSecurityException | CertificateVerificationException | NamingException ex)
+                {
+                    // e.g. LDAP behind corporate proxy
+                    // but couldn't get LDAP to work at all, see e.g. file from PDFBOX-1452
+                    LOG.warn("Caught " + ex.getClass().getSimpleName() + " downloading CRL, will try next distribution point if available");
+                    if (firstException == null)
+                    {
+                        firstException = ex;
+                    }
+                    continue;
+                }
+
+                Set<X509Certificate> mergedCertSet = CertificateVerifier.downloadExtraCertificates(crl);
+                mergedCertSet.addAll(additionalCerts);
+
+                // Verify CRL, see wikipedia:
+                // "To validate a specific CRL prior to relying on it,
+                //  the certificate of its corresponding CA is needed"
+                X509Certificate crlIssuerCert = null;
+                for (X509Certificate possibleCert : mergedCertSet)
+                {
+                    try
+                    {
+                        cert.verify(possibleCert.getPublicKey(), SecurityProvider.getProvider());
+                        crlIssuerCert = possibleCert;
+                        break;
+                    }
+                    catch (GeneralSecurityException ex)
+                    {
+                        // not the issuer
+                    }
+                }
+                if (crlIssuerCert == null)
+                {
+                    throw new CertificateVerificationException(
+                            "Certificate for " + crl.getIssuerX500Principal() +
+                            "not found in certificate chain, so the CRL at " +
+                            crlDistributionPointsURL + " could not be verified");
+                }
+                crl.verify(crlIssuerCert.getPublicKey(), SecurityProvider.getProvider());
+                //TODO these should be exceptions, but for that we need a test case where
+                // a PDF has a broken OCSP and a working CRL
+                if (crl.getThisUpdate().after(now))
+                {
+                    LOG.error("CRL not yet valid, thisUpdate is " + crl.getThisUpdate());
+                }
+                if (crl.getNextUpdate().before(now))
+                {
+                    LOG.error("CRL no longer valid, nextUpdate is " + crl.getNextUpdate());
+                }
+
+                if (!crl.getIssuerX500Principal().equals(cert.getIssuerX500Principal()))
+                {
+                    LOG.info("CRL issuer certificate is not identical to cert issuer, check needed");
+                    CertificateVerifier.verifyCertificate(crlIssuerCert, mergedCertSet, true, now);
+                    LOG.info("CRL issuer certificate checked successfully");
+                }
+                else
+                {
+                    LOG.info("CRL issuer certificate is identical to cert issuer, no extra check needed");
+                }
+
+                checkRevocation(crl, cert, signDate, crlDistributionPointsURL);
+
+                // https://tools.ietf.org/html/rfc5280#section-4.2.1.13
+                // If the DistributionPointName contains multiple values,
+                // each name describes a different mechanism to obtain the same
+                // CRL.  For example, the same CRL could be available for
+                // retrieval through both LDAP and HTTP.
+                //
+                // => thus no need to check several protocols
+                return;
+            }
+            if (firstException != null)
+            {
+                throw firstException;
+            }
+        }
+        catch (RevokedCertificateException | CertificateVerificationException ex)
+        {
+            throw ex;
+        }
+        catch (Exception ex)
+        {
+            throw new CertificateVerificationException(
+                    "Cannot verify CRL for certificate: "
+                    + cert.getSubjectX500Principal(), ex);
+
+        }
+    }
+
+    /**
+     * Check whether the certificate was revoked at signing time.
+     *
+     * @param crl certificate revocation list
+     * @param cert certificate to be checked
+     * @param signDate date the certificate was used for signing
+     * @param crlDistributionPointsURL URL for log message or exception text
+     * @throws RevokedCertificateException if the certificate was revoked at signing time
+     */
+    public static void checkRevocation(
+        X509CRL crl, X509Certificate cert, Date signDate, String crlDistributionPointsURL)
+                throws RevokedCertificateException
+    {
+        X509CRLEntry revokedCRLEntry = crl.getRevokedCertificate(cert);
+        if (revokedCRLEntry != null &&
+                revokedCRLEntry.getRevocationDate().compareTo(signDate) <= 0)
+        {
+            throw new RevokedCertificateException(
+                    "The certificate was revoked by CRL " +
+                            crlDistributionPointsURL + " on " + revokedCRLEntry.getRevocationDate(),
+                    revokedCRLEntry.getRevocationDate());
+        }
+        else if (revokedCRLEntry != null)
+        {
+            LOG.info("The certificate was revoked after signing by CRL " +
+                    crlDistributionPointsURL + " on " + revokedCRLEntry.getRevocationDate());
+        }
+        else
+        {
+            LOG.info("The certificate was not revoked by CRL " + crlDistributionPointsURL);
+        }
+    }
+
+    /**
+     * Downloads CRL from given URL. Supports http, https, ftp and ldap based URLs.
+     */
+    private static X509CRL downloadCRL(String crlURL) throws IOException,
+            CertificateException, CRLException,
+            CertificateVerificationException, NamingException
+    {
+        if (crlURL.startsWith("http://") || crlURL.startsWith("https://")
+                || crlURL.startsWith("ftp://"))
+        {
+            return downloadCRLFromWeb(crlURL);
+        }
+        else if (crlURL.startsWith("ldap://"))
+        {
+            return downloadCRLFromLDAP(crlURL);
+        }
+        else
+        {
+            throw new CertificateVerificationException(
+                    "Can not download CRL from certificate "
+                    + "distribution point: " + crlURL);
+        }
+    }
+
+    /**
+     * Downloads a CRL from given LDAP url, e.g.
+     * ldap://ldap.infonotary.com/dc=identity-ca,dc=infonotary,dc=com
+     */
+    private static X509CRL downloadCRLFromLDAP(String ldapURL) throws CertificateException,
+            NamingException, CRLException,
+            CertificateVerificationException
+    {
+        @SuppressWarnings({"squid:S1149"})
+        Hashtable<String, String> env = new Hashtable<>();
+        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+        env.put(Context.PROVIDER_URL, ldapURL);
+
+        // https://docs.oracle.com/javase/jndi/tutorial/ldap/connect/create.html
+        // don't wait forever behind corporate proxy
+        env.put("com.sun.jndi.ldap.connect.timeout", "1000");
+
+        DirContext ctx = new InitialDirContext(env);
+        Attributes avals = ctx.getAttributes("");
+        Attribute aval = avals.get("certificateRevocationList;binary");
+        byte[] val = (byte[]) aval.get();
+        if (val == null || val.length == 0)
+        {
+            throw new CertificateVerificationException("Can not download CRL from: " + ldapURL);
+        }
+        else
+        {
+            InputStream inStream = new ByteArrayInputStream(val);
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            return (X509CRL) cf.generateCRL(inStream);
+        }
+    }
+
+    /**
+     * Downloads a CRL from given HTTP/HTTPS/FTP URL, e.g.
+     * http://crl.infonotary.com/crl/identity-ca.crl
+     */
+    public static X509CRL downloadCRLFromWeb(String crlURL)
+            throws IOException, CertificateException, CRLException
+    {
+        try (InputStream crlStream = new URL(crlURL).openStream())
+        {
+            return (X509CRL) CertificateFactory.getInstance("X.509").generateCRL(crlStream);
+        }
+    }
+
+    /**
+     * Extracts all CRL distribution point URLs from the "CRL Distribution
+     * Point" extension in a X.509 certificate. If CRL distribution point
+     * extension is unavailable, returns an empty list.
+     * @param cert
+     * @return List of CRL distribution point URLs.
+     * @throws IOException
+     */
+    public static List<String> getCrlDistributionPoints(X509Certificate cert)
+            throws IOException
+    {
+        byte[] crldpExt = cert.getExtensionValue(Extension.cRLDistributionPoints.getId());
+        if (crldpExt == null)
+        {
+            return new ArrayList<>();
+        }
+        ASN1Primitive derObjCrlDP;
+        try (ASN1InputStream oAsnInStream = new ASN1InputStream(crldpExt))
+        {
+            derObjCrlDP = oAsnInStream.readObject();
+        }
+        if (!(derObjCrlDP instanceof ASN1OctetString))
+        {
+            LOG.warn("CRL distribution points for certificate subject " +
+                    cert.getSubjectX500Principal().getName() +
+                    " should be an octet string, but is " + derObjCrlDP);
+            return new ArrayList<>();
+        }
+        ASN1OctetString dosCrlDP = (ASN1OctetString) derObjCrlDP;
+        byte[] crldpExtOctets = dosCrlDP.getOctets();
+        ASN1Primitive derObj2;
+        try (ASN1InputStream oAsnInStream2 = new ASN1InputStream(crldpExtOctets))
+        {
+            derObj2 = oAsnInStream2.readObject();
+        }
+        CRLDistPoint distPoint = CRLDistPoint.getInstance(derObj2);
+        List<String> crlUrls = new ArrayList<>();
+        for (DistributionPoint dp : distPoint.getDistributionPoints())
+        {
+            DistributionPointName dpn = dp.getDistributionPoint();
+            // Look for URIs in fullName
+            if (dpn != null && dpn.getType() == DistributionPointName.FULL_NAME)
+            {
+                // Look for an URI
+                for (GeneralName genName : GeneralNames.getInstance(dpn.getName()).getNames())
+                {
+                    if (genName.getTagNo() == GeneralName.uniformResourceIdentifier)
+                    {
+                        String url = DERIA5String.getInstance(genName.getName()).getString();
+                        crlUrls.add(url);
+                    }
+                }
+            }
+        }
+        return crlUrls;
+    }
+}

+ 40 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/CertificateVerificationException.java

@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert;
+
+/**
+ * Copied from Apache CXF 2.4.9, initial version:
+ * https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.9/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/
+ * 
+ */
+public class CertificateVerificationException extends Exception
+{
+    private static final long serialVersionUID = 1L;
+
+    public CertificateVerificationException(String message, Throwable cause)
+    {
+        super(message, cause);
+    }
+
+    public CertificateVerificationException(String message)
+    {
+        super(message);
+    }
+}

+ 65 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/CertificateVerificationResult.java

@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert;
+
+import java.security.cert.PKIXCertPathBuilderResult;
+
+/**
+ * Copied from Apache CXF 2.4.9, initial version:
+ * https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.9/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/
+ * 
+ */
+public class CertificateVerificationResult
+{
+    private final boolean valid;
+    private PKIXCertPathBuilderResult result;
+    private Throwable exception;
+
+    /**
+     * Constructs a certificate verification result for valid certificate by
+     * given certification path.
+     */
+    public CertificateVerificationResult(PKIXCertPathBuilderResult result)
+    {
+        this.valid = true;
+        this.result = result;
+    }
+
+    public CertificateVerificationResult(Throwable exception)
+    {
+        this.valid = false;
+        this.exception = exception;
+    }
+
+    public boolean isValid()
+    {
+        return valid;
+    }
+
+    public PKIXCertPathBuilderResult getResult()
+    {
+        return result;
+    }
+
+    public Throwable getException()
+    {
+        return exception;
+    }
+}

+ 486 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/CertificateVerifier.java

@@ -0,0 +1,486 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.pdmodel.encryption.SecurityProvider;
+import org.bouncycastle.asn1.*;
+import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.cert.ocsp.BasicOCSPResp;
+import org.bouncycastle.cert.ocsp.OCSPException;
+import org.bouncycastle.cert.ocsp.OCSPResp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.*;
+import java.util.*;
+
+/**
+ * Copied from Apache CXF 2.4.9, initial version:
+ * https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.9/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/
+ * 
+ */
+public final class CertificateVerifier
+{
+    private static final Log LOG = LogFactory.getLog(CertificateVerifier.class);
+
+    private CertificateVerifier()
+    {
+
+    }
+
+    /**
+     * Attempts to build a certification chain for given certificate and to
+     * verify it. Relies on a set of root CA certificates and intermediate
+     * certificates that will be used for building the certification chain. The
+     * verification process assumes that all self-signed certificates in the set
+     * are trusted root CA certificates and all other certificates in the set
+     * are intermediate certificates.
+     *
+     * @param cert - certificate for validation
+     * @param additionalCerts - set of trusted root CA certificates that will be
+     * used as "trust anchors" and intermediate CA certificates that will be
+     * used as part of the certification chain. All self-signed certificates are
+     * considered to be trusted root CA certificates. All the rest are
+     * considered to be intermediate CA certificates.
+     * @param verifySelfSignedCert true if a self-signed certificate is accepted, false if not.
+     * @param signDate the date when the signing took place
+     * @return the certification chain (if verification is successful)
+     * @throws CertificateVerificationException - if the certification is not
+     * successful (e.g. certification path cannot be built or some certificate
+     * in the chain is expired or CRL checks are failed)
+     */
+    public static PKIXCertPathBuilderResult verifyCertificate(
+            X509Certificate cert, Set<X509Certificate> additionalCerts,
+            boolean verifySelfSignedCert, Date signDate)
+            throws CertificateVerificationException
+    {
+        try
+        {
+            // Check for self-signed certificate
+            if (!verifySelfSignedCert && isSelfSigned(cert))
+            {
+                throw new CertificateVerificationException("The certificate is self-signed.");
+            }
+
+            Set<X509Certificate> certSet = new HashSet<>(additionalCerts);
+
+            // Download extra certificates. However, each downloaded certificate can lead to
+            // more extra certificates, e.g. with the file from PDFBOX-4091, which has
+            // an incomplete chain.
+            // You can skip this block if you know that the certificate chain is complete
+            Set<X509Certificate> certsToTrySet = new HashSet<>();
+            certsToTrySet.add(cert);
+            certsToTrySet.addAll(additionalCerts);
+            int downloadSize = 0;
+            while (!certsToTrySet.isEmpty())
+            {
+                Set<X509Certificate> nextCertsToTrySet = new HashSet<>();
+                for (X509Certificate tryCert : certsToTrySet)
+                {
+                    Set<X509Certificate> downloadedExtraCertificatesSet =
+                            CertificateVerifier.downloadExtraCertificates(tryCert);
+                    for (X509Certificate downloadedCertificate : downloadedExtraCertificatesSet)
+                    {
+                        if (!certSet.contains(downloadedCertificate))
+                        {
+                            nextCertsToTrySet.add(downloadedCertificate);
+                            certSet.add(downloadedCertificate);
+                            downloadSize++;
+                        }
+                    }
+                }
+                certsToTrySet = nextCertsToTrySet;
+            }
+            if (downloadSize > 0)
+            {
+                LOG.info("CA issuers: " + downloadSize + " downloaded certificate(s) are new");
+            }
+
+            // Prepare a set of trust anchors (set of root CA certificates)
+            // and a set of intermediate certificates
+            Set<X509Certificate> intermediateCerts = new HashSet<>();
+            Set<TrustAnchor> trustAnchors = new HashSet<>();
+            for (X509Certificate additionalCert : certSet)
+            {
+                if (isSelfSigned(additionalCert))
+                {
+                    trustAnchors.add(new TrustAnchor(additionalCert, null));
+                }
+                else
+                {
+                    intermediateCerts.add(additionalCert);
+                }
+            }
+
+            if (trustAnchors.isEmpty())
+            {
+                throw new CertificateVerificationException("No root certificate in the chain");
+            }
+
+            // Attempt to build the certification chain and verify it
+            PKIXCertPathBuilderResult verifiedCertChain = verifyCertificate(
+                    cert, trustAnchors, intermediateCerts, signDate);
+
+            LOG.info("Certification chain verified successfully up to this root: " +
+                    verifiedCertChain.getTrustAnchor().getTrustedCert().getSubjectX500Principal());
+
+            checkRevocations(cert, certSet, signDate);
+
+            return verifiedCertChain;
+        }
+        catch (CertPathBuilderException certPathEx)
+        {
+            throw new CertificateVerificationException(
+                    "Error building certification path: "
+                    + cert.getSubjectX500Principal(), certPathEx);
+        }
+        catch (CertificateVerificationException cvex)
+        {
+            throw cvex;
+        }
+        catch (IOException | GeneralSecurityException | RevokedCertificateException | OCSPException ex)
+        {
+            throw new CertificateVerificationException(
+                    "Error verifying the certificate: "
+                    + cert.getSubjectX500Principal(), ex);
+        }
+    }
+
+    private static void checkRevocations(X509Certificate cert,
+                                         Set<X509Certificate> additionalCerts,
+                                         Date signDate)
+            throws IOException, CertificateVerificationException, OCSPException,
+                   RevokedCertificateException, GeneralSecurityException
+    {
+        if (isSelfSigned(cert))
+        {
+            // root, we're done
+            return;
+        }
+        X509Certificate issuerCert = null;
+        for (X509Certificate additionalCert : additionalCerts)
+        {
+            try
+            {
+                cert.verify(additionalCert.getPublicKey(), SecurityProvider.getProvider());
+                issuerCert = additionalCert;
+                break;
+            }
+            catch (GeneralSecurityException ex)
+            {
+                // not the issuer
+            }
+        }
+        // issuerCert is never null here. If it hadn't been found, then there wouldn't be a 
+        // verifiedCertChain earlier.
+
+        // Try checking the certificate through OCSP (faster than CRL)
+        String ocspURL = extractOCSPURL(cert);
+        if (ocspURL != null)
+        {
+            OcspHelper ocspHelper = new OcspHelper(cert, signDate, issuerCert, additionalCerts, ocspURL);
+            try
+            {
+                verifyOCSP(ocspHelper, additionalCerts);
+            }
+            catch (IOException | OCSPException ex)
+            {
+                // IOException happens with 021496.pdf because OCSP responder no longer exists
+                // OCSPException happens with QV_RCA1_RCA3_CPCPS_V4_11.pdf
+                LOG.warn("Exception trying OCSP, will try CRL", ex);
+                LOG.warn("Certificate# to check: " + cert.getSerialNumber().toString(16));
+                CRLVerifier.verifyCertificateCRLs(cert, signDate, additionalCerts);
+            }
+        }
+        else
+        {
+            LOG.info("OCSP not available, will try CRL");
+
+            // Check whether the certificate is revoked by the CRL
+            // given in its CRL distribution point extension
+            CRLVerifier.verifyCertificateCRLs(cert, signDate, additionalCerts);
+        }
+
+        // now check the issuer
+        checkRevocations(issuerCert, additionalCerts, signDate);
+    }
+
+    /**
+     * Checks whether given X.509 certificate is self-signed.
+     * @param cert The X.509 certificate to check.
+     * @return true if the certificate is self-signed, false if not.
+     * @throws GeneralSecurityException
+     */
+    public static boolean isSelfSigned(X509Certificate cert) throws GeneralSecurityException
+    {
+        try
+        {
+            // Try to verify certificate signature with its own public key
+            PublicKey key = cert.getPublicKey();
+            cert.verify(key, SecurityProvider.getProvider());
+            return true;
+        }
+        catch (SignatureException | InvalidKeyException | IOException ex)
+        {
+            // Invalid signature --> not self-signed
+            LOG.debug("Couldn't get signature information - returning false", ex);
+            return false;
+        }
+    }
+
+    /**
+     * Download extra certificates from the URI mentioned in id-ad-caIssuers in the "authority
+     * information access" extension. The method is lenient, i.e. catches all exceptions.
+     *
+     * @param ext an X509 object that can have extensions.
+     *
+     * @return a certificate set, never null.
+     */
+    public static Set<X509Certificate> downloadExtraCertificates(X509Extension ext)
+    {
+        // https://tools.ietf.org/html/rfc2459#section-4.2.2.1
+        // https://tools.ietf.org/html/rfc3280#section-4.2.2.1
+        // https://tools.ietf.org/html/rfc4325
+        Set<X509Certificate> resultSet = new HashSet<>();
+        byte[] authorityExtensionValue = ext.getExtensionValue(Extension.authorityInfoAccess.getId());
+        if (authorityExtensionValue == null)
+        {
+            return resultSet;
+        }
+        ASN1Primitive asn1Prim;
+        try
+        {
+            asn1Prim = JcaX509ExtensionUtils.parseExtensionValue(authorityExtensionValue);
+        }
+        catch (IOException ex)
+        {
+            LOG.warn(ex.getMessage(), ex);
+            return resultSet;
+        }
+        if (!(asn1Prim instanceof ASN1Sequence))
+        {
+            LOG.warn("ASN1Sequence expected, got " + asn1Prim.getClass().getSimpleName());
+            return resultSet;
+        }
+        ASN1Sequence asn1Seq = (ASN1Sequence) asn1Prim;
+        Enumeration<?> objects = asn1Seq.getObjects();
+        while (objects.hasMoreElements())
+        {
+            // AccessDescription
+            ASN1Sequence obj = (ASN1Sequence) objects.nextElement();
+            ASN1Encodable oid = obj.getObjectAt(0);
+            if (!X509ObjectIdentifiers.id_ad_caIssuers.equals(oid))
+            {
+                continue;
+            }
+            ASN1TaggedObject location = (ASN1TaggedObject) obj.getObjectAt(1);
+            ASN1OctetString uri = (ASN1OctetString) location.getObject();
+            String urlString = new String(uri.getOctets());
+            LOG.info("CA issuers URL: " + urlString);
+            try (InputStream in = new URL(urlString).openStream())
+            {
+                CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+                Collection<? extends Certificate> altCerts = certFactory.generateCertificates(in);
+                altCerts.forEach(altCert -> resultSet.add((X509Certificate) altCert));
+                LOG.info("CA issuers URL: " + altCerts.size() + " certificate(s) downloaded");
+            }
+            catch (IOException ex)
+            {
+                LOG.warn(urlString + " failure: " + ex.getMessage(), ex);
+            }
+            catch (CertificateException ex)
+            {
+                LOG.warn(ex.getMessage(), ex);
+            }
+        }
+        LOG.info("CA issuers: Downloaded " + resultSet.size() + " certificate(s) total");
+        return resultSet;
+    }
+
+    /**
+     * Attempts to build a certification chain for given certificate and to
+     * verify it. Relies on a set of root CA certificates (trust anchors) and a
+     * set of intermediate certificates (to be used as part of the chain).
+     *
+     * @param cert - certificate for validation
+     * @param trustAnchors - set of trust anchors
+     * @param intermediateCerts - set of intermediate certificates
+     * @param signDate the date when the signing took place
+     * @return the certification chain (if verification is successful)
+     * @throws GeneralSecurityException - if the verification is not successful
+     * (e.g. certification path cannot be built or some certificate in the chain
+     * is expired)
+     */
+    private static PKIXCertPathBuilderResult verifyCertificate(
+            X509Certificate cert, Set<TrustAnchor> trustAnchors,
+            Set<X509Certificate> intermediateCerts, Date signDate)
+            throws GeneralSecurityException
+    {
+        // Create the selector that specifies the starting certificate
+        X509CertSelector selector = new X509CertSelector();
+        selector.setCertificate(cert);
+
+        // Configure the PKIX certificate builder algorithm parameters
+        PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
+
+        // Disable CRL checks (this is done manually as additional step)
+        pkixParams.setRevocationEnabled(false);
+
+        // not doing this brings
+        // "SunCertPathBuilderException: unable to find valid certification path to requested target"
+        // (when using -Djava.security.debug=certpath: "critical policy qualifiers present in certificate")
+        // for files like 021496.pdf that have the "Adobe CDS Certificate Policy" 1.2.840.113583.1.2.1
+        // CDS = "Certified Document Services"
+        // https://www.adobe.com/misc/pdfs/Adobe_CDS_CP.pdf
+        pkixParams.setPolicyQualifiersRejected(false);
+        // However, maybe there is still work to do:
+        // "If the policyQualifiersRejected flag is set to false, it is up to the application
+        // to validate all policy qualifiers in this manner in order to be PKIX compliant."
+
+        pkixParams.setDate(signDate);
+
+        // Specify a list of intermediate certificates
+        CertStore intermediateCertStore = CertStore.getInstance("Collection",
+                new CollectionCertStoreParameters(intermediateCerts));
+        pkixParams.addCertStore(intermediateCertStore);
+
+        // Build and verify the certification chain
+        // If this doesn't work although it should, it can be debugged
+        // by starting java with -Djava.security.debug=certpath
+        // see also
+        // https://docs.oracle.com/javase/8/docs/technotes/guides/security/troubleshooting-security.html
+        CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
+        return (PKIXCertPathBuilderResult) builder.build(pkixParams);
+    }
+
+    /**
+     * Extract the OCSP URL from an X.509 certificate if available.
+     *
+     * @param cert X.509 certificate
+     * @return the URL of the OCSP validation service
+     * @throws IOException 
+     */
+    private static String extractOCSPURL(X509Certificate cert) throws IOException
+    {
+        byte[] authorityExtensionValue = cert.getExtensionValue(Extension.authorityInfoAccess.getId());
+        if (authorityExtensionValue != null)
+        {
+            // copied from CertInformationHelper.getAuthorityInfoExtensionValue()
+            // DRY refactor should be done some day
+            ASN1Sequence asn1Seq = (ASN1Sequence) JcaX509ExtensionUtils.parseExtensionValue(authorityExtensionValue);
+            Enumeration<?> objects = asn1Seq.getObjects();
+            while (objects.hasMoreElements())
+            {
+                // AccessDescription
+                ASN1Sequence obj = (ASN1Sequence) objects.nextElement();
+                ASN1Encodable oid = obj.getObjectAt(0);
+                // accessLocation
+                ASN1TaggedObject location = (ASN1TaggedObject) obj.getObjectAt(1);
+                if (X509ObjectIdentifiers.id_ad_ocsp.equals(oid)
+                        && location.getTagNo() == GeneralName.uniformResourceIdentifier)
+                {
+                    ASN1OctetString url = (ASN1OctetString) location.getObject();
+                    String ocspURL = new String(url.getOctets());
+                    LOG.info("OCSP URL: " + ocspURL);
+                    return ocspURL;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Verify whether the certificate has been revoked at signing date, and verify whether
+     * the certificate of the responder has been revoked now.
+     *
+     * @param ocspHelper the OCSP helper.
+     * @param additionalCerts
+     * @throws RevokedCertificateException
+     * @throws IOException
+     * @throws OCSPException
+     * @throws CertificateVerificationException
+     */
+    private static void verifyOCSP(OcspHelper ocspHelper, Set<X509Certificate> additionalCerts)
+            throws RevokedCertificateException, IOException, OCSPException, CertificateVerificationException
+    {
+        Date now = Calendar.getInstance().getTime();
+        OCSPResp ocspResponse;
+        ocspResponse = ocspHelper.getResponseOcsp();
+        if (ocspResponse.getStatus() != OCSPResp.SUCCESSFUL)
+        {
+            throw new CertificateVerificationException("OCSP check not successful, status: "
+                    + ocspResponse.getStatus());
+        }
+        LOG.info("OCSP check successful");
+
+        BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResponse.getResponseObject();
+        X509Certificate ocspResponderCertificate = ocspHelper.getOcspResponderCertificate();
+        if (ocspResponderCertificate.getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck.getId()) != null)
+        {
+            // https://tools.ietf.org/html/rfc6960#section-4.2.2.2.1
+            // A CA may specify that an OCSP client can trust a responder for the
+            // lifetime of the responder's certificate.  The CA does so by
+            // including the extension id-pkix-ocsp-nocheck.
+            LOG.info("Revocation check of OCSP responder certificate skipped (id-pkix-ocsp-nocheck is set)");
+            return;
+        }
+
+        if (ocspHelper.getCertificateToCheck().equals(ocspResponderCertificate))
+        {
+            LOG.info("OCSP responder certificate is identical to certificate to check");
+            return;
+        }
+
+        LOG.info("Check of OCSP responder certificate");
+        Set<X509Certificate> additionalCerts2 = new HashSet<>(additionalCerts);
+        JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
+        for (X509CertificateHolder certHolder : basicResponse.getCerts())
+        {
+            try
+            {
+                X509Certificate cert = certificateConverter.getCertificate(certHolder);
+                if (!ocspResponderCertificate.equals(cert))
+                {
+                    additionalCerts2.add(cert);
+                }
+            }
+            catch (CertificateException ex)
+            {
+                // unlikely to happen because the certificate existed as an object
+                LOG.error(ex, ex);
+            }
+        }
+        CertificateVerifier.verifyCertificate(ocspResponderCertificate, additionalCerts2, true, now);
+        LOG.info("Check of OCSP responder certificate done");
+    }
+}

+ 610 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/OcspHelper.java

@@ -0,0 +1,610 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert;
+
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.SigUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.pdmodel.encryption.SecurityProvider;
+import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.DLSequence;
+import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
+import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
+import org.bouncycastle.asn1.ocsp.ResponderID;
+import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.Extensions;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.bouncycastle.cert.ocsp.*;
+import org.bouncycastle.operator.ContentVerifierProvider;
+import org.bouncycastle.operator.DigestCalculator;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.*;
+
+/**
+ * Helper Class for OCSP-Operations with bouncy castle.
+ * 
+ * @author Alexis Suter
+ */
+public class OcspHelper
+{
+    private static final Log LOG = LogFactory.getLog(OcspHelper.class);
+
+    private final X509Certificate issuerCertificate;
+    private final Date signDate;
+    private final X509Certificate certificateToCheck;
+    private final Set<X509Certificate> additionalCerts;
+    private final String ocspUrl;
+    private DEROctetString encodedNonce;
+    private X509Certificate ocspResponderCertificate;
+    private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
+    
+    // SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
+    private static final Random RANDOM = new SecureRandom();
+
+    /**
+     * @param checkCertificate Certificate to be OCSP-checked
+     * @param signDate the date when the signing took place
+     * @param issuerCertificate Certificate of the issuer
+     * @param additionalCerts Set of trusted root CA certificates that will be used as "trust
+     * anchors" and intermediate CA certificates that will be used as part of the certification
+     * chain. All self-signed certificates are considered to be trusted root CA certificates. All
+     * the rest are considered to be intermediate CA certificates.
+     * @param ocspUrl where to fetch for OCSP
+     */
+    public OcspHelper(X509Certificate checkCertificate, Date signDate, X509Certificate issuerCertificate,
+            Set<X509Certificate> additionalCerts, String ocspUrl)
+    {
+        this.certificateToCheck = checkCertificate;
+        this.signDate = signDate;
+        this.issuerCertificate = issuerCertificate;
+        this.additionalCerts = additionalCerts;
+        this.ocspUrl = ocspUrl;
+    }
+
+    /**
+     * Get the certificate to be OCSP-checked.
+     * 
+     * @return The certificate to be OCSP-checked.
+     */
+    X509Certificate getCertificateToCheck()
+    {
+        return certificateToCheck;
+    }
+
+    /**
+     * Performs and verifies the OCSP-Request
+     *
+     * @return the OCSPResp, when the request was successful, else a corresponding exception will be
+     * thrown. Never returns null.
+     *
+     * @throws IOException
+     * @throws OCSPException
+     * @throws RevokedCertificateException
+     */
+    public OCSPResp getResponseOcsp() throws IOException, OCSPException, RevokedCertificateException
+    {
+        OCSPResp ocspResponse = performRequest();
+        verifyOcspResponse(ocspResponse);
+        return ocspResponse;
+    }
+
+    /**
+     * Get responder certificate. This is available after {@link #getResponseOcsp()} has been
+     * called. This method should be used instead of {@code basicResponse.getCerts()[0]}
+     *
+     * @return The certificate of the responder.
+     */
+    public X509Certificate getOcspResponderCertificate()
+    {
+        return ocspResponderCertificate;
+    }
+
+    /**
+     * Verifies the status and the response itself (including nonce), but not the signature.
+     * 
+     * @param ocspResponse to be verified
+     * @throws OCSPException
+     * @throws RevokedCertificateException
+     * @throws IOException if the default security provider can't be instantiated
+     */
+    private void verifyOcspResponse(OCSPResp ocspResponse)
+            throws OCSPException, RevokedCertificateException, IOException
+    {
+        verifyRespStatus(ocspResponse);
+
+        BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResponse.getResponseObject();
+        if (basicResponse != null)
+        {
+            ResponderID responderID = basicResponse.getResponderId().toASN1Primitive();
+            // https://tools.ietf.org/html/rfc6960#section-4.2.2.3
+            // The basic response type contains:
+            // (...)
+            // either the name of the responder or a hash of the responder's
+            // public key as the ResponderID
+            // (...)
+            // The responder MAY include certificates in the certs field of
+            // BasicOCSPResponse that help the OCSP client verify the responder's
+            // signature.
+            X500Name name = responderID.getName();
+            if (name != null)
+            {
+                findResponderCertificateByName(basicResponse, name);
+            }
+            else
+            {
+                byte[] keyHash = responderID.getKeyHash();
+                if (keyHash != null)
+                {
+                    findResponderCertificateByKeyHash(basicResponse, keyHash);
+                }
+                else
+                {
+                    throw new OCSPException("OCSP: basic response must provide name or key hash");
+                }
+            }
+
+            if (ocspResponderCertificate == null)
+            {
+                throw new OCSPException("OCSP: certificate for responder " + name + " not found");
+            }
+
+            try
+            {
+                SigUtils.checkResponderCertificateUsage(ocspResponderCertificate);
+            }
+            catch (CertificateParsingException ex)
+            {
+                // unlikely to happen because the certificate existed as an object
+                LOG.error(ex, ex);
+            }
+            checkOcspSignature(ocspResponderCertificate, basicResponse);
+
+            boolean nonceChecked = checkNonce(basicResponse);
+
+            SingleResp[] responses = basicResponse.getResponses();
+            if (responses.length != 1)
+            {
+                throw new OCSPException(
+                        "OCSP: Received " + responses.length + " responses instead of 1!");
+            }
+
+            SingleResp resp = responses[0];
+            Object status = resp.getCertStatus();
+
+            if (!nonceChecked)
+            {
+                // https://tools.ietf.org/html/rfc5019
+                // fall back to validating the OCSPResponse based on time
+                checkOcspResponseFresh(resp);
+            }
+
+            if (status instanceof RevokedStatus)
+            {
+                RevokedStatus revokedStatus = (RevokedStatus) status;
+                if (revokedStatus.getRevocationTime().compareTo(signDate) <= 0)
+                {
+                    throw new RevokedCertificateException(
+                        "OCSP: Certificate is revoked since " +
+                                revokedStatus.getRevocationTime(),
+                                revokedStatus.getRevocationTime());
+                }
+                LOG.info("The certificate was revoked after signing by OCSP " + ocspUrl + 
+                         " on " + revokedStatus.getRevocationTime());
+            }
+            else if (status != CertificateStatus.GOOD)
+            {
+                throw new OCSPException("OCSP: Status of Cert is unknown");
+            }
+        }
+    }
+
+    private byte[] getKeyHashFromCertHolder(X509CertificateHolder certHolder)
+    {
+        // https://tools.ietf.org/html/rfc2560#section-4.2.1
+        // KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key
+        //         -- (i.e., the SHA-1 hash of the value of the
+        //         -- BIT STRING subjectPublicKey [excluding
+        //         -- the tag, length, and number of unused
+        //         -- bits] in the responder's certificate)
+
+        // code below inspired by org.bouncycastle.cert.ocsp.CertificateID.createCertID()
+        // tested with SO52757037-Signed3-OCSP-with-KeyHash.pdf
+        SubjectPublicKeyInfo info = certHolder.getSubjectPublicKeyInfo();
+        try
+        {
+            return MessageDigest.getInstance("SHA-1").digest(info.getPublicKeyData().getBytes());
+        }
+        catch (NoSuchAlgorithmException ex)
+        {
+            // should not happen
+            LOG.error("SHA-1 Algorithm not found", ex);
+            return new byte[0];
+        }
+    }
+
+    private void findResponderCertificateByKeyHash(BasicOCSPResp basicResponse, byte[] keyHash)
+            throws IOException
+    {
+        X509CertificateHolder[] certHolders = basicResponse.getCerts();
+        for (X509CertificateHolder certHolder : certHolders)
+        {
+            byte[] digest = getKeyHashFromCertHolder(certHolder);
+            if (Arrays.equals(keyHash, digest))
+            {
+                try
+                {
+                    ocspResponderCertificate = certificateConverter.getCertificate(certHolder);
+                    return;
+                }
+                catch (CertificateException ex)
+                {
+                    // unlikely to happen because the certificate existed as an object
+                    LOG.error(ex, ex);
+                }
+                break;
+            }
+        }
+
+        // DO NOT use the certificate found in additionalCerts first. One file had a
+        // responder certificate in the PDF itself with SHA1withRSA algorithm, but
+        // the responder delivered a different (newer, more secure) certificate
+        // with SHA256withRSA (tried with QV_RCA1_RCA3_CPCPS_V4_11.pdf)
+        // https://www.quovadisglobal.com/~/media/Files/Repository/QV_RCA1_RCA3_CPCPS_V4_11.ashx
+        for (X509Certificate cert : additionalCerts)
+        {
+            try
+            {
+                byte[] digest = getKeyHashFromCertHolder(new X509CertificateHolder(cert.getEncoded()));
+                if (Arrays.equals(keyHash, digest))
+                {
+                    ocspResponderCertificate = cert;
+                    return;
+                }
+            }
+            catch (CertificateEncodingException ex)
+            {
+                // unlikely to happen because the certificate existed as an object
+                LOG.error(ex, ex);
+            }
+        }
+    }
+
+    private void findResponderCertificateByName(BasicOCSPResp basicResponse, X500Name name)
+    {
+        X509CertificateHolder[] certHolders = basicResponse.getCerts();
+        for (X509CertificateHolder certHolder : certHolders)
+        {
+            if (name.equals(certHolder.getSubject()))
+            {
+                try
+                {
+                    ocspResponderCertificate = certificateConverter.getCertificate(certHolder);
+                    return;
+                }
+                catch (CertificateException ex)
+                {
+                    // unlikely to happen because the certificate existed as an object
+                    LOG.error(ex, ex);
+                }
+            }
+        }
+
+        // DO NOT use the certificate found in additionalCerts first. One file had a
+        // responder certificate in the PDF itself with SHA1withRSA algorithm, but
+        // the responder delivered a different (newer, more secure) certificate
+        // with SHA256withRSA (tried with QV_RCA1_RCA3_CPCPS_V4_11.pdf)
+        // https://www.quovadisglobal.com/~/media/Files/Repository/QV_RCA1_RCA3_CPCPS_V4_11.ashx
+        for (X509Certificate cert : additionalCerts)
+        {
+            X500Name certSubjectName = new X500Name(cert.getSubjectX500Principal().getName());
+            if (certSubjectName.equals(name))
+            {
+                ocspResponderCertificate = cert;
+                return;
+            }
+        }
+    }
+
+    private void checkOcspResponseFresh(SingleResp resp) throws OCSPException
+    {
+        // https://tools.ietf.org/html/rfc5019
+        // Clients MUST check for the existence of the nextUpdate field and MUST
+        // ensure the current time, expressed in GMT time as described in
+        // Section 2.2.4, falls between the thisUpdate and nextUpdate times.  If
+        // the nextUpdate field is absent, the client MUST reject the response.
+
+        Date curDate = Calendar.getInstance().getTime();
+
+        Date thisUpdate = resp.getThisUpdate();
+        if (thisUpdate == null)
+        {
+            throw new OCSPException("OCSP: thisUpdate field is missing in response (RFC 5019 2.2.4.)");
+        }
+        Date nextUpdate = resp.getNextUpdate();
+        if (nextUpdate == null)
+        {
+            throw new OCSPException("OCSP: nextUpdate field is missing in response (RFC 5019 2.2.4.)");
+        }
+        if (curDate.compareTo(thisUpdate) < 0)
+        {
+            LOG.error(curDate + " < " + thisUpdate);
+            throw new OCSPException("OCSP: current date < thisUpdate field (RFC 5019 2.2.4.)");
+        }
+        if (curDate.compareTo(nextUpdate) > 0)
+        {
+            LOG.error(curDate + " > " + nextUpdate);
+            throw new OCSPException("OCSP: current date > nextUpdate field (RFC 5019 2.2.4.)");
+        }
+        LOG.info("OCSP response is fresh");
+    }
+
+    /**
+     * Checks whether the OCSP response is signed by the given certificate.
+     * 
+     * @param certificate the certificate to check the signature
+     * @param basicResponse OCSP response containing the signature
+     * @throws OCSPException when the signature is invalid or could not be checked
+     * @throws IOException if the default security provider can't be instantiated
+     */
+    private void checkOcspSignature(X509Certificate certificate, BasicOCSPResp basicResponse)
+            throws OCSPException, IOException
+    {
+        try
+        {
+            ContentVerifierProvider verifier = new JcaContentVerifierProviderBuilder()
+                    .setProvider(SecurityProvider.getProvider()).build(certificate);
+
+            if (!basicResponse.isSignatureValid(verifier))
+            {
+                throw new OCSPException("OCSP-Signature is not valid!");
+            }
+        }
+        catch (OperatorCreationException e)
+        {
+            throw new OCSPException("Error checking Ocsp-Signature", e);
+        }
+    }
+
+    /**
+     * Checks if the nonce in the response matches.
+     * 
+     * @param basicResponse Response to be checked
+     * @return true if the nonce is present and matches, false if nonce is missing.
+     * @throws OCSPException if the nonce is different
+     */
+    private boolean checkNonce(BasicOCSPResp basicResponse) throws OCSPException
+    {
+        Extension nonceExt = basicResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
+        if (nonceExt != null)
+        {
+            DEROctetString responseNonceString = (DEROctetString) nonceExt.getExtnValue();
+            if (!responseNonceString.equals(encodedNonce))
+            {
+                throw new OCSPException("Different nonce found in response!");
+            }
+            else
+            {
+                LOG.info("Nonce is good");
+                return true;
+            }
+        }
+        // https://tools.ietf.org/html/rfc5019
+        // Clients that opt to include a nonce in the
+        // request SHOULD NOT reject a corresponding OCSPResponse solely on the
+        // basis of the nonexistent expected nonce, but MUST fall back to
+        // validating the OCSPResponse based on time.
+        return false;
+    }
+
+    /**
+     * Performs the OCSP-Request, with given data.
+     * 
+     * @return the OCSPResp, that has been fetched from the ocspUrl
+     * @throws IOException
+     * @throws OCSPException
+     */
+    private OCSPResp performRequest() throws IOException, OCSPException
+    {
+        OCSPReq request = generateOCSPRequest();
+        URL url = new URL(ocspUrl);
+        HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
+        try
+        {
+            httpConnection.setRequestProperty("Content-Type", "application/ocsp-request");
+            httpConnection.setRequestProperty("Accept", "application/ocsp-response");
+            httpConnection.setDoOutput(true);
+            try (OutputStream out = httpConnection.getOutputStream())
+            {
+                out.write(request.getEncoded());
+            }
+
+            if (httpConnection.getResponseCode() != 200)
+            {
+                throw new IOException("OCSP: Could not access url, ResponseCode: "
+                        + httpConnection.getResponseCode());
+            }
+            // Get response
+            try (InputStream in = (InputStream) httpConnection.getContent())
+            {
+                return new OCSPResp(in);
+            }
+        }
+        finally
+        {
+            httpConnection.disconnect();
+        }
+    }
+
+    /**
+     * Helper method to verify response status.
+     * 
+     * @param resp OCSP response
+     * @throws OCSPException if the response status is not ok
+     */
+    public void verifyRespStatus(OCSPResp resp) throws OCSPException
+    {
+        String statusInfo = "";
+        if (resp != null)
+        {
+            int status = resp.getStatus();
+            switch (status)
+            {
+            case OCSPResponseStatus.INTERNAL_ERROR:
+                statusInfo = "INTERNAL_ERROR";
+                LOG.error("An internal error occurred in the OCSP Server!");
+                break;
+            case OCSPResponseStatus.MALFORMED_REQUEST:
+                // This happened when the "critical" flag was used for extensions
+                // on a responder known by the committer of this comment.
+                statusInfo = "MALFORMED_REQUEST";
+                LOG.error("Your request did not fit the RFC 2560 syntax!");
+                break;
+            case OCSPResponseStatus.SIG_REQUIRED:
+                statusInfo = "SIG_REQUIRED";
+                LOG.error("Your request was not signed!");
+                break;
+            case OCSPResponseStatus.TRY_LATER:
+                statusInfo = "TRY_LATER";
+                LOG.error("The server was too busy to answer you!");
+                break;
+            case OCSPResponseStatus.UNAUTHORIZED:
+                statusInfo = "UNAUTHORIZED";
+                LOG.error("The server could not authenticate you!");
+                break;
+            case OCSPResponseStatus.SUCCESSFUL:
+                break;
+            default:
+                statusInfo = "UNKNOWN";
+                LOG.error("Unknown OCSPResponse status code! " + status);
+            }
+        }
+        if (resp == null || resp.getStatus() != OCSPResponseStatus.SUCCESSFUL)
+        {
+            throw new OCSPException("OCSP response unsuccessful, status: " + statusInfo);
+        }
+    }
+
+    /**
+     * Generates an OCSP request and generates the <code>CertificateID</code>.
+     *
+     * @return OCSP request, ready to fetch data
+     * @throws OCSPException
+     * @throws IOException
+     */
+    private OCSPReq generateOCSPRequest() throws OCSPException, IOException
+    {
+        Security.addProvider(SecurityProvider.getProvider());
+
+        // Generate the ID for the certificate we are looking for
+        CertificateID certId;
+        try
+        {
+            certId = new CertificateID(new SHA1DigestCalculator(),
+                    new JcaX509CertificateHolder(issuerCertificate),
+                    certificateToCheck.getSerialNumber());
+        }
+        catch (CertificateEncodingException e)
+        {
+            throw new IOException("Error creating CertificateID with the Certificate encoding", e);
+        }
+
+        // https://tools.ietf.org/html/rfc2560#section-4.1.2
+        // Support for any specific extension is OPTIONAL. The critical flag
+        // SHOULD NOT be set for any of them.
+
+        Extension responseExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_response,
+                false, new DLSequence(OCSPObjectIdentifiers.id_pkix_ocsp_basic).getEncoded());
+
+        encodedNonce = new DEROctetString(new DEROctetString(create16BytesNonce()));
+        Extension nonceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false,
+                encodedNonce);
+
+        OCSPReqBuilder builder = new OCSPReqBuilder();
+        builder.setRequestExtensions(
+                new Extensions(new Extension[] { responseExtension, nonceExtension }));
+        builder.addRequest(certId);
+        return builder.build();
+    }
+
+    private byte[] create16BytesNonce()
+    {
+        byte[] nonce = new byte[16];
+        RANDOM.nextBytes(nonce);
+        return nonce;
+    }
+
+    /**
+     * Class to create SHA-1 Digest, used for creation of CertificateID.
+     */
+    private static class SHA1DigestCalculator implements DigestCalculator
+    {
+        private final ByteArrayOutputStream bOut = new ByteArrayOutputStream();
+
+        @Override
+        public AlgorithmIdentifier getAlgorithmIdentifier()
+        {
+            return new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1);
+        }
+
+        @Override
+        public OutputStream getOutputStream()
+        {
+            return bOut;
+        }
+
+        @Override
+        public byte[] getDigest()
+        {
+            byte[] bytes = bOut.toByteArray();
+            bOut.reset();
+
+            try
+            {
+                MessageDigest md = MessageDigest.getInstance("SHA-1");
+                return md.digest(bytes);
+            }
+            catch (NoSuchAlgorithmException ex)
+            {
+                // should not happen
+                LOG.error("SHA-1 Algorithm not found", ex);
+                return new byte[0];
+            }
+        }
+    }
+}

+ 48 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/cert/RevokedCertificateException.java

@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert;
+
+import java.util.Date;
+
+/**
+ * Exception to handle a revoked Certificate explicitly
+ * 
+ * @author Alexis Suter
+ */
+public class RevokedCertificateException extends Exception
+{
+    private static final long serialVersionUID = 3543946618794126654L;
+    
+    private final Date revocationTime;
+
+    public RevokedCertificateException(String message)
+    {
+        super(message);
+        this.revocationTime = null;
+    }
+
+    public RevokedCertificateException(String message, Date revocationTime)
+    {
+        super(message);
+        this.revocationTime = revocationTime;
+    }
+
+    public Date getRevocationTime()
+    {
+        return revocationTime;
+    }
+}

+ 25 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/package.html

@@ -0,0 +1,25 @@
+<!--
+ ! Licensed to the Apache Software Foundation (ASF) under one or more
+ ! contributor license agreements.  See the NOTICE file distributed with
+ ! this work for additional information regarding copyright ownership.
+ ! The ASF licenses this file to You under the Apache License, Version 2.0
+ ! (the "License"); you may not use this file except in compliance with
+ ! the License.  You may obtain a copy of the License at
+ !
+ !      http://www.apache.org/licenses/LICENSE-2.0
+ !
+ ! Unless required by applicable law or agreed to in writing, software
+ ! distributed under the License is distributed on an "AS IS" BASIS,
+ ! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ! See the License for the specific language governing permissions and
+ ! limitations under the License.
+ !-->
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html lang="en">
+<head><title></title>
+
+</head>
+<body>
+These examples will show how to gain access to the PDF signature.
+</body>
+</html>

+ 601 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/validation/AddValidationInformation.java

@@ -0,0 +1,601 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.validation;
+
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.SigUtils;
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert.CRLVerifier;
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert.CertificateVerificationException;
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert.OcspHelper;
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert.RevokedCertificateException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.cos.*;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
+import org.apache.pdfbox.pdmodel.encryption.SecurityProvider;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
+import org.apache.pdfbox.util.Hex;
+import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
+import org.bouncycastle.cert.ocsp.BasicOCSPResp;
+import org.bouncycastle.cert.ocsp.OCSPException;
+import org.bouncycastle.cert.ocsp.OCSPResp;
+
+import java.io.*;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509CRL;
+import java.security.cert.X509Certificate;
+import java.util.Calendar;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An example for adding Validation Information to a signed PDF, inspired by ETSI TS 102 778-4
+ * V1.1.2 (2009-12), Part 4: PAdES Long Term - PAdES-LTV Profile. This procedure appends the
+ * Validation Information of the last signature (more precise its signer(s)) to a copy of the
+ * document. The signature and the signed data will not be touched and stay valid.
+ * <p>
+ * See also <a href="http://eprints.hsr.ch/id/eprint/616">Bachelor thesis (in German) about LTV</a>
+ *
+ * @author Alexis Suter
+ */
+public class AddValidationInformation
+{
+    private static final Log LOG = LogFactory.getLog(AddValidationInformation.class);
+
+    private CertInformationCollector certInformationHelper;
+    private COSArray correspondingOCSPs;
+    private COSArray correspondingCRLs;
+    private COSDictionary vriBase;
+    private COSArray ocsps;
+    private COSArray crls;
+    private COSArray certs;
+    private PDDocument document;
+    private final Set<X509Certificate> foundRevocationInformation = new HashSet<>();
+    private Calendar signDate;
+    private final Set<X509Certificate> ocspChecked = new HashSet<>();
+    //TODO foundRevocationInformation and ocspChecked have a similar purpose. One of them should likely
+    // be removed and the code improved. When doing so, keep in mind that ocspChecked was added last,
+    // because of a problem with freetsa.
+
+    /**
+     * Signs the given PDF file.
+     * 
+     * @param inFile input PDF file
+     * @param outFile output PDF file
+     * @throws IOException if the input file could not be read
+     */
+    public void validateSignature(File inFile, File outFile) throws IOException
+    {
+        if (inFile == null || !inFile.exists())
+        {
+            String err = "Document for signing ";
+            if (null == inFile)
+            {
+                err += "is null";
+            }
+            else
+            {
+                err += "does not exist: " + inFile.getAbsolutePath();
+            }
+            throw new FileNotFoundException(err);
+        }
+
+        try (PDDocument doc = Loader.loadPDF(inFile);
+             FileOutputStream fos = new FileOutputStream(outFile))
+        {
+            int accessPermissions = SigUtils.getMDPPermission(doc);
+            if (accessPermissions == 1)
+            {
+                System.out.println("PDF is certified to forbid changes, "
+                        + "some readers may report the document as invalid despite that "
+                        + "the PDF specification allows DSS additions");
+            }
+            document = doc;
+            doValidation(inFile.getAbsolutePath(), fos);
+        }
+    }
+
+    /**
+     * Fetches certificate information from the last signature of the document and appends a DSS
+     * with the validation information to the document.
+     *
+     * @param filename in file to extract signature
+     * @param output where to write the changed document
+     * @throws IOException
+     */
+    private void doValidation(String filename, OutputStream output) throws IOException
+    {
+        certInformationHelper = new CertInformationCollector();
+        CertInformationCollector.CertSignatureInformation certInfo = null;
+        try
+        {
+            PDSignature signature = SigUtils.getLastRelevantSignature(document);
+            if (signature != null)
+            {
+                certInfo = certInformationHelper.getLastCertInfo(signature, filename);
+                signDate = signature.getSignDate();
+            }
+        }
+        catch (CertificateProccessingException e)
+        {
+            throw new IOException("An Error occurred processing the Signature", e);
+        }
+        if (certInfo == null)
+        {
+            throw new IOException(
+                    "No Certificate information or signature found in the given document");
+        }
+
+        PDDocumentCatalog docCatalog = document.getDocumentCatalog();
+        COSDictionary catalog = docCatalog.getCOSObject();
+        catalog.setNeedToBeUpdated(true);
+
+        COSDictionary dss = getOrCreateDictionaryEntry(COSDictionary.class, catalog, "DSS");
+
+        addExtensions(docCatalog);
+
+        vriBase = getOrCreateDictionaryEntry(COSDictionary.class, dss, "VRI");
+
+        ocsps = getOrCreateDictionaryEntry(COSArray.class, dss, "OCSPs");
+
+        crls = getOrCreateDictionaryEntry(COSArray.class, dss, "CRLs");
+
+        certs = getOrCreateDictionaryEntry(COSArray.class, dss, "Certs");
+
+        addRevocationData(certInfo);
+
+        addAllCertsToCertArray();
+
+        // write incremental
+        document.saveIncremental(output);
+    }
+
+    /**
+     * Gets or creates a dictionary entry. If existing checks for the type and sets need to be
+     * updated.
+     *
+     * @param clazz the class of the dictionary entry, must implement COSUpdateInfo
+     * @param parent where to find the element
+     * @param name of the element
+     * @return a Element of given class, new or existing
+     * @throws IOException when the type of the element is wrong
+     */
+    private static <T extends COSBase & COSUpdateInfo> T getOrCreateDictionaryEntry(Class<T> clazz,
+            COSDictionary parent, String name) throws IOException
+    {
+        T result;
+        COSBase element = parent.getDictionaryObject(name);
+        if (element != null && clazz.isInstance(element))
+        {
+            result = clazz.cast(element);
+            result.setNeedToBeUpdated(true);
+        }
+        else if (element != null)
+        {
+            throw new IOException("Element " + name + " from dictionary is not of type "
+                    + clazz.getCanonicalName());
+        }
+        else
+        {
+            try
+            {
+                result = clazz.getDeclaredConstructor().newInstance();
+            }
+            catch (ReflectiveOperationException | SecurityException e)
+            {
+                throw new IOException("Failed to create new instance of " + clazz.getCanonicalName(), e);
+            }
+            result.setDirect(false);
+            parent.setItem(COSName.getPDFName(name), result);
+        }
+        return result;
+    }
+
+    /**
+     * Fetches and adds revocation information based on the certInfo to the DSS.
+     *
+     * @param certInfo Certificate information from CertInformationHelper containing certificate
+     * chains.
+     * @throws IOException
+     */
+    private void addRevocationData(CertInformationCollector.CertSignatureInformation certInfo) throws IOException
+    {
+        COSDictionary vri = new COSDictionary();
+        vriBase.setItem(certInfo.getSignatureHash(), vri);
+
+        updateVRI(certInfo, vri);
+
+        if (certInfo.getTsaCerts() != null)
+        {
+            // Don't add RevocationInfo from tsa to VRI's
+            correspondingOCSPs = null;
+            correspondingCRLs = null;
+            addRevocationDataRecursive(certInfo.getTsaCerts());
+        }
+    }
+
+    /**
+     * Tries to get Revocation Data (first OCSP, else CRL) from the given Certificate Chain.
+     *
+     * @param certInfo from which to fetch revocation data. Will work recursively through its
+     * chains.
+     * @throws IOException when failed to fetch an revocation data.
+     */
+    private void addRevocationDataRecursive(CertInformationCollector.CertSignatureInformation certInfo) throws IOException
+    {
+        if (certInfo.isSelfSigned())
+        {
+            return;
+        }
+        // To avoid getting same revocation information twice.
+        boolean isRevocationInfoFound = foundRevocationInformation.contains(certInfo.getCertificate());
+        if (!isRevocationInfoFound)
+        {
+            if (certInfo.getOcspUrl() != null && certInfo.getIssuerCertificate() != null)
+            {
+                isRevocationInfoFound = fetchOcspData(certInfo);
+            }
+            if (!isRevocationInfoFound && certInfo.getCrlUrl() != null)
+            {
+                fetchCrlData(certInfo);
+                isRevocationInfoFound = true;
+            }
+
+            if (certInfo.getOcspUrl() == null && certInfo.getCrlUrl() == null)
+            {
+                LOG.info("No revocation information for cert " + certInfo.getCertificate().getSubjectX500Principal());
+            }
+            else if (!isRevocationInfoFound)
+            {
+                throw new IOException("Could not fetch Revocation Info for Cert: "
+                        + certInfo.getCertificate().getSubjectX500Principal());
+            }
+        }
+
+        if (certInfo.getAlternativeCertChain() != null)
+        {
+            addRevocationDataRecursive(certInfo.getAlternativeCertChain());
+        }
+
+        if (certInfo.getCertChain() != null && certInfo.getCertChain().getCertificate() != null)
+        {
+            addRevocationDataRecursive(certInfo.getCertChain());
+        }
+    }
+
+    /**
+     * Tries to fetch and add OCSP Data to its containers.
+     *
+     * @param certInfo the certificate info, for it to check OCSP data.
+     * @return true when the OCSP data has successfully been fetched and added
+     * @throws IOException when Certificate is revoked.
+     */
+    private boolean fetchOcspData(CertInformationCollector.CertSignatureInformation certInfo) throws IOException
+    {
+        try
+        {
+            addOcspData(certInfo);
+            return true;
+        }
+        catch (OCSPException | CertificateProccessingException | IOException e)
+        {
+            LOG.warn("Failed fetching Ocsp", e);
+            return false;
+        }
+        catch (RevokedCertificateException e)
+        {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Tries to fetch and add CRL Data to its containers.
+     *
+     * @param certInfo the certificate info, for it to check CRL data.
+     * @throws IOException when failed to fetch, because no validation data could be fetched for
+     * data.
+     */
+    private void fetchCrlData(CertInformationCollector.CertSignatureInformation certInfo) throws IOException
+    {
+        try
+        {
+            addCrlRevocationInfo(certInfo);
+        }
+        catch (GeneralSecurityException | IOException | RevokedCertificateException | CertificateVerificationException e)
+        {
+            LOG.warn("Failed fetching CRL", e);
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Fetches and adds OCSP data to storage for the given Certificate.
+     * 
+     * @param certInfo the certificate info, for it to check OCSP data.
+     * @throws IOException
+     * @throws OCSPException
+     * @throws CertificateProccessingException
+     * @throws RevokedCertificateException
+     */
+    private void addOcspData(CertInformationCollector.CertSignatureInformation certInfo) throws IOException, OCSPException,
+            CertificateProccessingException, RevokedCertificateException
+    {
+        if (ocspChecked.contains(certInfo.getCertificate()))
+        {
+            // This certificate has been OCSP-checked before
+            return;
+        }
+        OcspHelper ocspHelper = new OcspHelper(
+                certInfo.getCertificate(),
+                signDate.getTime(),
+                certInfo.getIssuerCertificate(),
+                new HashSet<>(certInformationHelper.getCertificateSet()),
+                certInfo.getOcspUrl());
+        OCSPResp ocspResp = ocspHelper.getResponseOcsp();
+        ocspChecked.add(certInfo.getCertificate());
+        BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResp.getResponseObject();
+        X509Certificate ocspResponderCertificate = ocspHelper.getOcspResponderCertificate();
+        certInformationHelper.addAllCertsFromHolders(basicResponse.getCerts());
+        byte[] signatureHash;
+        try
+        {
+            signatureHash = MessageDigest.getInstance("SHA-1").digest(basicResponse.getSignature());
+        }
+        catch (NoSuchAlgorithmException ex)
+        {
+            throw new CertificateProccessingException(ex);
+        }
+        String signatureHashHex = Hex.getString(signatureHash);
+
+        if (!vriBase.containsKey(signatureHashHex))
+        {
+            COSArray savedCorrespondingOCSPs = correspondingOCSPs;
+            COSArray savedCorrespondingCRLs = correspondingCRLs;
+
+            COSDictionary vri = new COSDictionary();
+            vriBase.setItem(signatureHashHex, vri);
+            CertInformationCollector.CertSignatureInformation ocspCertInfo = certInformationHelper.getCertInfo(ocspResponderCertificate);
+
+            updateVRI(ocspCertInfo, vri);
+
+            correspondingOCSPs = savedCorrespondingOCSPs;
+            correspondingCRLs = savedCorrespondingCRLs;
+        }
+
+        byte[] ocspData = ocspResp.getEncoded();
+
+        COSStream ocspStream = writeDataToStream(ocspData);
+        ocsps.add(ocspStream);
+        if (correspondingOCSPs != null)
+        {
+            correspondingOCSPs.add(ocspStream);
+        }
+        foundRevocationInformation.add(certInfo.getCertificate());
+    }
+
+    /**
+     * Fetches and adds CRL data to storage for the given Certificate.
+     * 
+     * @param certInfo the certificate info, for it to check CRL data.
+     * @throws IOException
+     * @throws RevokedCertificateException
+     * @throws GeneralSecurityException
+     * @throws CertificateVerificationException 
+     */
+    private void addCrlRevocationInfo(CertInformationCollector.CertSignatureInformation certInfo)
+            throws IOException, RevokedCertificateException, GeneralSecurityException,
+            CertificateVerificationException
+    {
+        X509CRL crl = CRLVerifier.downloadCRLFromWeb(certInfo.getCrlUrl());
+        X509Certificate issuerCertificate = certInfo.getIssuerCertificate();
+
+        // find the issuer certificate (usually issuer of signature certificate)
+        for (X509Certificate certificate : certInformationHelper.getCertificateSet())
+        {
+            if (certificate.getSubjectX500Principal().equals(crl.getIssuerX500Principal()))
+            {
+                issuerCertificate = certificate;
+                break;
+            }
+        }
+        crl.verify(issuerCertificate.getPublicKey(), SecurityProvider.getProvider().getName());
+        CRLVerifier.checkRevocation(crl, certInfo.getCertificate(), signDate.getTime(), certInfo.getCrlUrl());
+        COSStream crlStream = writeDataToStream(crl.getEncoded());
+        crls.add(crlStream);
+        if (correspondingCRLs != null)
+        {
+            correspondingCRLs.add(crlStream);
+
+            byte[] signatureHash;
+            try
+            {
+                signatureHash = MessageDigest.getInstance("SHA-1").digest(crl.getSignature());
+            }
+            catch (NoSuchAlgorithmException ex)
+            {
+                throw new CertificateVerificationException(ex.getMessage(), ex);
+            }
+            String signatureHashHex = Hex.getString(signatureHash);
+
+            if (!vriBase.containsKey(signatureHashHex))
+            {
+                COSArray savedCorrespondingOCSPs = correspondingOCSPs;
+                COSArray savedCorrespondingCRLs = correspondingCRLs;
+
+                COSDictionary vri = new COSDictionary();
+                vriBase.setItem(signatureHashHex, vri);
+
+                CertInformationCollector.CertSignatureInformation crlCertInfo;
+                try
+                {
+                    crlCertInfo = certInformationHelper.getCertInfo(issuerCertificate);
+                }
+                catch (CertificateProccessingException ex)
+                {
+                    throw new CertificateVerificationException(ex.getMessage(), ex);
+                }
+
+                updateVRI(crlCertInfo, vri);
+
+                correspondingOCSPs = savedCorrespondingOCSPs;
+                correspondingCRLs = savedCorrespondingCRLs;
+            }
+        }
+        foundRevocationInformation.add(certInfo.getCertificate());
+    }
+
+    private void updateVRI(CertInformationCollector.CertSignatureInformation certInfo, COSDictionary vri) throws IOException
+    {
+        if (certInfo.getCertificate().getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck.getId()) == null)
+        {
+            correspondingOCSPs = new COSArray();
+            correspondingCRLs = new COSArray();
+            addRevocationDataRecursive(certInfo);
+            if (correspondingOCSPs.size() > 0)
+            {
+                vri.setItem("OCSP", correspondingOCSPs);
+            }
+            if (correspondingCRLs.size() > 0)
+            {
+                vri.setItem("CRL", correspondingCRLs);
+            }
+        }
+
+        COSArray correspondingCerts = new COSArray();
+        CertInformationCollector.CertSignatureInformation ci = certInfo;
+        do
+        {
+            X509Certificate cert = ci.getCertificate();
+            try
+            {
+                COSStream certStream = writeDataToStream(cert.getEncoded());
+                correspondingCerts.add(certStream);
+                certs.add(certStream); // may lead to duplicate certificates. Important?
+            }
+            catch (CertificateEncodingException ex)
+            {
+                // should not happen because these are existing certificates
+                LOG.error(ex, ex);
+            }
+
+            if (cert.getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck.getId()) != null)
+            {
+                break;
+            }
+            ci = ci.getCertChain();
+        }
+        while (ci != null);
+        vri.setItem(COSName.CERT, correspondingCerts);
+
+        vri.setDate(COSName.TU, Calendar.getInstance());
+    }
+
+    /**
+     * Adds all certs to the certs-array. Make sure, all certificates are inside the
+     * certificateStore of certInformationHelper
+     *
+     * @throws IOException
+     */
+    private void addAllCertsToCertArray() throws IOException
+    {
+        try
+        {
+            for (X509Certificate cert : certInformationHelper.getCertificateSet())
+            {
+                COSStream stream = writeDataToStream(cert.getEncoded());
+                certs.add(stream);
+            }
+        }
+        catch (CertificateEncodingException e)
+        {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Creates a Flate encoded <code>COSStream</code> object with the given data.
+     * 
+     * @param data to write into the COSStream
+     * @return COSStream a COSStream object that can be added to the document
+     * @throws IOException
+     */
+    private COSStream writeDataToStream(byte[] data) throws IOException
+    {
+        COSStream stream = document.getDocument().createCOSStream();
+        try (OutputStream os = stream.createOutputStream(COSName.FLATE_DECODE))
+        {
+            os.write(data);
+        }
+        return stream;
+    }
+
+    /**
+     * Adds Extensions to the document catalog. So that the use of DSS is identified. Described in
+     * PAdES Part 4, Chapter 4.4.
+     *
+     * @param catalog to add Extensions into
+     */
+    private void addExtensions(PDDocumentCatalog catalog)
+    {
+        COSDictionary dssExtensions = new COSDictionary();
+        dssExtensions.setDirect(true);
+        catalog.getCOSObject().setItem("Extensions", dssExtensions);
+
+        COSDictionary adbeExtension = new COSDictionary();
+        adbeExtension.setDirect(true);
+        dssExtensions.setItem("ADBE", adbeExtension);
+
+        adbeExtension.setName("BaseVersion", "1.7");
+        adbeExtension.setInt("ExtensionLevel", 5);
+
+        catalog.setVersion("1.7");
+    }
+
+    public static void main(String[] args) throws IOException
+    {
+        if (args.length != 1)
+        {
+            usage();
+            System.exit(1);
+        }
+
+        // register BouncyCastle provider, needed for "exotic" algorithms
+        Security.addProvider(SecurityProvider.getProvider());
+
+        // add ocspInformation
+        AddValidationInformation addOcspInformation = new AddValidationInformation();
+
+        File inFile = new File(args[0]);
+        String name = inFile.getName();
+        String substring = name.substring(0, name.lastIndexOf('.'));
+
+        File outFile = new File(inFile.getParent(), substring + "_LTV.pdf");
+        addOcspInformation.validateSignature(inFile, outFile);
+    }
+
+    private static void usage()
+    {
+        System.err.println("usage: java " + AddValidationInformation.class.getName() + " "
+                + "<pdf_to_add_ocsp>\n");
+    }
+}

+ 466 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/validation/CertInformationCollector.java

@@ -0,0 +1,466 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.validation;
+
+import com.x.processplatform.assemble.surface.jaxrs.attachment.signature.cert.CertificateVerifier;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.pdmodel.encryption.SecurityProvider;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1Object;
+import org.bouncycastle.asn1.cms.Attribute;
+import org.bouncycastle.asn1.cms.AttributeTable;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.SignerInformation;
+import org.bouncycastle.util.Selector;
+import org.bouncycastle.util.Store;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class helps to extract data/information from a signature. The information is held in
+ * CertSignatureInformation. Some information is needed for validation processing of the
+ * participating certificates.
+ *
+ * @author Alexis Suter
+ *
+ */
+public class CertInformationCollector
+{
+    private static final Log LOG = LogFactory.getLog(CertInformationCollector.class);
+
+    private static final int MAX_CERTIFICATE_CHAIN_DEPTH = 5;
+
+    private final Set<X509Certificate> certificateSet = new HashSet<>();
+    private final Set<String> urlSet = new HashSet<>();
+
+    private final JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter();
+
+    private CertSignatureInformation rootCertInfo;
+
+    /**
+     * Gets the certificate information of a signature.
+     * 
+     * @param signature the signature of the document.
+     * @param fileName of the document.
+     * @return the CertSignatureInformation containing all certificate information
+     * @throws CertificateProccessingException when there is an error processing the certificates
+     * @throws IOException on a data processing error
+     */
+    public CertSignatureInformation getLastCertInfo(PDSignature signature, String fileName)
+            throws CertificateProccessingException, IOException
+    {
+        try (FileInputStream documentInput = new FileInputStream(fileName))
+        {
+            byte[] signatureContent = signature.getContents(documentInput);
+            return getCertInfo(signatureContent);
+        }
+    }
+
+    /**
+     * Processes one signature and its including certificates.
+     *
+     * @param signatureContent the byte[]-Content of the signature
+     * @return the CertSignatureInformation for this signature
+     * @throws IOException
+     * @throws CertificateProccessingException
+     */
+    private CertSignatureInformation getCertInfo(byte[] signatureContent)
+            throws CertificateProccessingException, IOException
+    {
+        rootCertInfo = new CertSignatureInformation();
+
+        rootCertInfo.signatureHash = CertInformationHelper.getSha1Hash(signatureContent);
+
+        try
+        {
+            CMSSignedData signedData = new CMSSignedData(signatureContent);
+            SignerInformation signerInformation = processSignerStore(signedData, rootCertInfo);
+            addTimestampCerts(signerInformation);
+        }
+        catch (CMSException e)
+        {
+            LOG.error("Error occurred getting Certificate Information from Signature", e);
+            throw new CertificateProccessingException(e);
+        }
+        return rootCertInfo;
+    }
+
+    /**
+     * Processes an embedded signed timestamp, that has been placed into a signature. The
+     * certificates and its chain(s) will be processed the same way as the signature itself.
+     *
+     * @param signerInformation of the signature, to get unsigned attributes from it.
+     * @throws IOException
+     * @throws CertificateProccessingException
+     */
+    private void addTimestampCerts(SignerInformation signerInformation)
+            throws IOException, CertificateProccessingException
+    {
+        AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
+        if (unsignedAttributes == null)
+        {
+            return;
+        }
+        Attribute tsAttribute = unsignedAttributes
+                .get(PKCSObjectIdentifiers.id_aa_signatureTimeStampToken);
+        if (tsAttribute == null)
+        {
+            return;
+        }
+        ASN1Encodable obj0 = tsAttribute.getAttrValues().getObjectAt(0);
+        if (!(obj0 instanceof ASN1Object))
+        {
+            return;
+        }
+        ASN1Object tsSeq = (ASN1Object) obj0;
+
+        try
+        {
+            CMSSignedData signedData = new CMSSignedData(tsSeq.getEncoded("DER"));
+            rootCertInfo.tsaCerts = new CertSignatureInformation();
+            processSignerStore(signedData, rootCertInfo.tsaCerts);
+        }
+        catch (CMSException e)
+        {
+            throw new IOException("Error parsing timestamp token", e);
+        }
+    }
+
+    /**
+     * Processes a signer store and goes through the signers certificate-chain. Adds the found data
+     * to the certInfo. Handles only the first signer, although multiple would be possible, but is
+     * not yet practicable.
+     *
+     * @param signedData data from which to get the SignerInformation
+     * @param certInfo where to add certificate information
+     * @return Signer Information of the processed certificatesStore for further usage.
+     * @throws IOException on data-processing error
+     * @throws CertificateProccessingException on a specific error with a certificate
+     */
+    private SignerInformation processSignerStore(
+            CMSSignedData signedData, CertSignatureInformation certInfo)
+            throws IOException, CertificateProccessingException
+    {
+        Collection<SignerInformation> signers = signedData.getSignerInfos().getSigners();
+        SignerInformation signerInformation = signers.iterator().next();
+
+        @SuppressWarnings("unchecked")
+        Store<X509CertificateHolder> certificatesStore = signedData.getCertificates();
+        @SuppressWarnings("unchecked")
+        Collection<X509CertificateHolder> matches = certificatesStore
+                .getMatches((Selector<X509CertificateHolder>) signerInformation.getSID());
+
+        X509Certificate certificate = getCertFromHolder(matches.iterator().next());
+        certificateSet.add(certificate);
+
+        Collection<X509CertificateHolder> allCerts = certificatesStore.getMatches(null);
+        addAllCerts(allCerts);
+        traverseChain(certificate, certInfo, MAX_CERTIFICATE_CHAIN_DEPTH);
+        return signerInformation;
+    }
+
+    /**
+     * Traverse through the Cert-Chain of the given Certificate and add it to the CertInfo
+     * recursively.
+     *
+     * @param certificate Actual Certificate to be processed
+     * @param certInfo where to add the Certificate (and chain) information
+     * @param maxDepth Max depth from this point to go through CertChain (could be infinite)
+     * @throws IOException on data-processing error
+     * @throws CertificateProccessingException on a specific error with a certificate
+     */
+    private void traverseChain(X509Certificate certificate, CertSignatureInformation certInfo,
+            int maxDepth) throws IOException, CertificateProccessingException
+    {
+        certInfo.certificate = certificate;
+
+        // Certificate Authority Information Access
+        // As described in https://tools.ietf.org/html/rfc3280.html#section-4.2.2.1
+        byte[] authorityExtensionValue = certificate.getExtensionValue(Extension.authorityInfoAccess.getId());
+        if (authorityExtensionValue != null)
+        {
+            CertInformationHelper.getAuthorityInfoExtensionValue(authorityExtensionValue, certInfo);
+        }
+
+        if (certInfo.issuerUrl != null)
+        {
+            getAlternativeIssuerCertificate(certInfo, maxDepth);
+        }
+
+        // As described in https://tools.ietf.org/html/rfc3280.html#section-4.2.1.14
+        byte[] crlExtensionValue = certificate.getExtensionValue(Extension.cRLDistributionPoints.getId());
+        if (crlExtensionValue != null)
+        {
+            certInfo.crlUrl = CertInformationHelper.getCrlUrlFromExtensionValue(crlExtensionValue);
+        }
+
+        try
+        {
+            certInfo.isSelfSigned = CertificateVerifier.isSelfSigned(certificate);
+        }
+        catch (GeneralSecurityException ex)
+        {
+            throw new CertificateProccessingException(ex);
+        }
+        if (maxDepth <= 0 || certInfo.isSelfSigned)
+        {
+            return;
+        }
+
+        for (X509Certificate issuer : certificateSet)
+        {
+            try
+            {
+                certificate.verify(issuer.getPublicKey(), SecurityProvider.getProvider());
+                LOG.info("Found the right Issuer Cert! for Cert: " + certificate.getSubjectX500Principal()
+                    + "\n" + issuer.getSubjectX500Principal());
+                certInfo.issuerCertificate = issuer;
+                certInfo.certChain = new CertSignatureInformation();
+                traverseChain(issuer, certInfo.certChain, maxDepth - 1);
+                break;
+            }
+            catch (GeneralSecurityException ex)
+            {
+                // not the issuer
+            }                
+        }
+        if (certInfo.issuerCertificate == null)
+        {
+            throw new IOException(
+                    "No Issuer Certificate found for Cert: '" +
+                            certificate.getSubjectX500Principal() + "', i.e. Cert '" +
+                            certificate.getIssuerX500Principal() + "' is missing in the chain");
+        }
+    }
+
+    /**
+     * Get alternative certificate chain, from the Authority Information (a url). If the chain is
+     * not included in the signature, this is the main chain. Otherwise there might be a second
+     * chain. Exceptions which happen on this chain will be logged and ignored, because the cert
+     * might not be available at the time or other reasons.
+     *
+     * @param certInfo base Certificate Information, on which to put the alternative Certificate
+     * @param maxDepth Maximum depth to dig through the chain from here on.
+     * @throws CertificateProccessingException on a specific error with a certificate
+     */
+    private void getAlternativeIssuerCertificate(CertSignatureInformation certInfo, int maxDepth)
+            throws CertificateProccessingException
+    {
+        if (urlSet.contains(certInfo.issuerUrl))
+        {
+            return;
+        }
+        urlSet.add(certInfo.issuerUrl);
+        LOG.info("Get alternative issuer certificate from: " + certInfo.issuerUrl);
+        try
+        {
+            URL certUrl = new URL(certInfo.issuerUrl);
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            try (InputStream in = certUrl.openStream())
+            {
+                X509Certificate altIssuerCert = (X509Certificate) certFactory
+                        .generateCertificate(in);
+                certificateSet.add(altIssuerCert);
+
+                certInfo.alternativeCertChain = new CertSignatureInformation();
+                traverseChain(altIssuerCert, certInfo.alternativeCertChain, maxDepth - 1);
+            }
+        }
+        catch (IOException | CertificateException e)
+        {
+            LOG.error("Error getting alternative issuer certificate from " + certInfo.issuerUrl, e);
+        }
+    }
+
+    /**
+     * Gets the X509Certificate out of the X509CertificateHolder.
+     *
+     * @param certificateHolder to get the certificate from
+     * @return a X509Certificate or <code>null</code> when there was an Error with the Certificate
+     * @throws CertificateProccessingException on failed conversion from X509CertificateHolder to
+     * X509Certificate
+     */
+    private X509Certificate getCertFromHolder(X509CertificateHolder certificateHolder)
+            throws CertificateProccessingException
+    {
+        try
+        {
+            return certConverter.getCertificate(certificateHolder);
+        }
+        catch (CertificateException e)
+        {
+            LOG.error("Certificate Exception getting Certificate from certHolder.", e);
+            throw new CertificateProccessingException(e);
+        }
+    }
+
+    /**
+     * Adds multiple Certificates out of a Collection of X509CertificateHolder into certificateSet.
+     *
+     * @param certHolders Collection of X509CertificateHolder
+     */
+    private void addAllCerts(Collection<X509CertificateHolder> certHolders)
+    {
+        for (X509CertificateHolder certificateHolder : certHolders)
+        {
+            try
+            {
+                X509Certificate certificate = getCertFromHolder(certificateHolder);
+                certificateSet.add(certificate);
+            }
+            catch (CertificateProccessingException e)
+            {
+                LOG.warn("Certificate Exception getting Certificate from certHolder.", e);
+            }
+        }
+    }
+
+    /**
+     * Gets a list of X509Certificate out of an array of X509CertificateHolder. The certificates
+     * will be added to certificateSet.
+     *
+     * @param certHolders Array of X509CertificateHolder
+     * @throws CertificateProccessingException when one of the Certificates could not be parsed.
+     */
+    public void addAllCertsFromHolders(X509CertificateHolder[] certHolders)
+            throws CertificateProccessingException
+    {
+        addAllCerts(Arrays.asList(certHolders));
+    }
+
+    /**
+     * Traverse a certificate.
+     *
+     * @param certificate
+     * @return
+     * @throws CertificateProccessingException 
+     */
+    CertSignatureInformation getCertInfo(X509Certificate certificate) throws CertificateProccessingException
+    {
+        try
+        {
+            CertSignatureInformation certSignatureInformation = new CertSignatureInformation();
+            traverseChain(certificate, certSignatureInformation, MAX_CERTIFICATE_CHAIN_DEPTH);
+            return certSignatureInformation;
+        }
+        catch (IOException ex)
+        {
+            throw new CertificateProccessingException(ex);
+        }
+    }
+
+    /**
+     * Get the set of all processed certificates until now.
+     * 
+     * @return a set of serial numbers to certificates.
+     */
+    public Set<X509Certificate> getCertificateSet()
+    {
+        return certificateSet;
+    }
+
+    /**
+     * Data class to hold Signature, Certificate (and its chain(s)) and revocation Information
+     */
+    public static class CertSignatureInformation
+    {
+        private X509Certificate certificate;
+        private String signatureHash;
+        private boolean isSelfSigned = false;
+        private String ocspUrl;
+        private String crlUrl;
+        private String issuerUrl;
+        private X509Certificate issuerCertificate;
+        private CertSignatureInformation certChain;
+        private CertSignatureInformation tsaCerts;
+        private CertSignatureInformation alternativeCertChain;
+
+        public String getOcspUrl()
+        {
+            return ocspUrl;
+        }
+
+        public void setOcspUrl(String ocspUrl)
+        {
+            this.ocspUrl = ocspUrl;
+        }
+
+        public void setIssuerUrl(String issuerUrl)
+        {
+            this.issuerUrl = issuerUrl;
+        }
+
+        public String getCrlUrl()
+        {
+            return crlUrl;
+        }
+
+        public X509Certificate getCertificate()
+        {
+            return certificate;
+        }
+
+        public boolean isSelfSigned()
+        {
+            return isSelfSigned;
+        }
+
+        public X509Certificate getIssuerCertificate()
+        {
+            return issuerCertificate;
+        }
+
+        public String getSignatureHash()
+        {
+            return signatureHash;
+        }
+
+        public CertSignatureInformation getCertChain()
+        {
+            return certChain;
+        }
+
+        public CertSignatureInformation getTsaCerts()
+        {
+            return tsaCerts;
+        }
+
+        public CertSignatureInformation getAlternativeCertChain()
+        {
+            return alternativeCertChain;
+        }
+    }
+}

+ 165 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/validation/CertInformationHelper.java

@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.validation;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.util.Hex;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1OctetString;
+import org.bouncycastle.asn1.ASN1Sequence;
+import org.bouncycastle.asn1.ASN1TaggedObject;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Enumeration;
+
+public class CertInformationHelper
+{
+    private static final Log LOG = LogFactory.getLog(CertInformationHelper.class);
+
+    private CertInformationHelper()
+    {
+    }
+
+    /**
+     * Gets the SHA-1-Hash has of given byte[]-content.
+     * 
+     * @param content to be hashed
+     * @return SHA-1 hash String
+     */
+    protected static String getSha1Hash(byte[] content)
+    {
+        try
+        {
+            MessageDigest md = MessageDigest.getInstance("SHA-1");
+            return Hex.getString(md.digest(content));
+        }
+        catch (NoSuchAlgorithmException e)
+        {
+            LOG.error("No SHA-1 Algorithm found", e);
+        }
+        return null;
+    }
+
+    /**
+     * Extracts authority information access extension values from the given data. The Data
+     * structure has to be implemented as described in RFC 2459, 4.2.2.1.
+     *
+     * @param extensionValue byte[] of the extension value.
+     * @param certInfo where to put the found values
+     * @throws IOException when there is a problem with the extensionValue
+     */
+    protected static void getAuthorityInfoExtensionValue(byte[] extensionValue,
+            CertInformationCollector.CertSignatureInformation certInfo) throws IOException
+    {
+        ASN1Sequence asn1Seq = (ASN1Sequence) JcaX509ExtensionUtils.parseExtensionValue(extensionValue);
+        Enumeration<?> objects = asn1Seq.getObjects();
+        while (objects.hasMoreElements())
+        {
+            // AccessDescription
+            ASN1Sequence obj = (ASN1Sequence) objects.nextElement();
+            ASN1Encodable oid = obj.getObjectAt(0);
+            // accessLocation
+            ASN1TaggedObject location = (ASN1TaggedObject) obj.getObjectAt(1);
+
+            if (X509ObjectIdentifiers.id_ad_ocsp.equals(oid)
+                    && location.getTagNo() == GeneralName.uniformResourceIdentifier)
+            {
+                ASN1OctetString url = (ASN1OctetString) location.getObject();
+                certInfo.setOcspUrl(new String(url.getOctets()));
+            }
+            else if (X509ObjectIdentifiers.id_ad_caIssuers.equals(oid))
+            {
+                ASN1OctetString uri = (ASN1OctetString) location.getObject();
+                certInfo.setIssuerUrl(new String(uri.getOctets()));
+            }
+        }
+    }
+
+    /**
+     * Gets the first CRL URL from given extension value. Structure has to be
+     * built as in 4.2.1.14 CRL Distribution Points of RFC 2459.
+     *
+     * @param extensionValue to get the extension value from
+     * @return first CRL- URL or null
+     * @throws IOException when there is a problem with the extensionValue
+     */
+    protected static String getCrlUrlFromExtensionValue(byte[] extensionValue) throws IOException
+    {
+        ASN1Sequence asn1Seq = (ASN1Sequence) JcaX509ExtensionUtils.parseExtensionValue(extensionValue);
+        Enumeration<?> objects = asn1Seq.getObjects();
+
+        while (objects.hasMoreElements())
+        {
+            Object obj = objects.nextElement();
+            if (obj instanceof ASN1Sequence)
+            {
+                String url = extractCrlUrlFromSequence((ASN1Sequence) obj);
+                if (url != null)
+                {
+                    return url;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static String extractCrlUrlFromSequence(ASN1Sequence sequence)
+    {
+        ASN1TaggedObject taggedObject = (ASN1TaggedObject) sequence.getObjectAt(0);
+        taggedObject = (ASN1TaggedObject) taggedObject.getObject();
+        if (taggedObject.getObject() instanceof ASN1TaggedObject)
+        {
+            taggedObject = (ASN1TaggedObject) taggedObject.getObject();
+        }
+        else if (taggedObject.getObject() instanceof ASN1Sequence)
+        {
+            // multiple URLs (we take the first)
+            ASN1Sequence seq = (ASN1Sequence) taggedObject.getObject();
+            if (seq.getObjectAt(0) instanceof ASN1TaggedObject)
+            {
+                taggedObject = (ASN1TaggedObject) seq.getObjectAt(0);
+            }
+            else
+            {
+                return null;
+            }
+        }
+        else
+        {
+            return null;
+        }
+        if (taggedObject.getObject() instanceof ASN1OctetString)
+        {
+            ASN1OctetString uri = (ASN1OctetString) taggedObject.getObject();
+            String url = new String(uri.getOctets());
+
+            // return first http(s)-Url for crl
+            if (url.startsWith("http"))
+            {
+                return url;
+            }
+        }
+        // else happens with http://blogs.adobe.com/security/SampleSignedPDFDocument.pdf
+        return null;
+    }
+}

+ 32 - 0
o2server/x_processplatform_assemble_surface/src/main/java/com/x/processplatform/assemble/surface/jaxrs/attachment/signature/validation/CertificateProccessingException.java

@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.x.processplatform.assemble.surface.jaxrs.attachment.signature.validation;
+
+/**
+ * Class to wrap around Certificate Processing exceptions
+ * 
+ * @author Alexis Suter
+ */
+public class CertificateProccessingException extends Exception
+{
+    private static final long serialVersionUID = 814859842830313903L;
+
+    public CertificateProccessingException(Throwable cause)
+    {
+        super(cause);
+    }
+}