]> source.dussan.org Git - xmlgraphics-fop.git/commitdiff
FOP-3166: Add option to sign PDF
authorSimon Steiner <ssteiner@apache.org>
Wed, 7 Feb 2024 10:29:57 +0000 (10:29 +0000)
committerSimon Steiner <ssteiner@apache.org>
Wed, 7 Feb 2024 10:29:57 +0000 (10:29 +0000)
12 files changed:
fop-core/pom.xml
fop-core/src/main/java/org/apache/fop/pdf/CMSProcessableInputStream.java [new file with mode: 0644]
fop-core/src/main/java/org/apache/fop/pdf/PDFSignParams.java [new file with mode: 0644]
fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java [new file with mode: 0644]
fop-core/src/main/java/org/apache/fop/render/pdf/PDFDocumentHandler.java
fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java
fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java
fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java
fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java
fop-core/src/main/java/org/apache/fop/render/pdf/PDFSignOption.java [new file with mode: 0644]
fop-core/src/test/java/org/apache/fop/pdf/PDFSigningTestCase.java [new file with mode: 0644]
fop-core/src/test/resources/org/apache/fop/pdf/keystore.pkcs12 [new file with mode: 0644]

index bc2c56781b4a96dc9837a57d8c4ce28c4ee1e204..d1e1a2f650b243e9512d39e72602c89e6cae0f6a 100644 (file)
       <version>1.1.3</version>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcpkix-jdk15to18</artifactId>
+      <version>1.77</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk15to18</artifactId>
+      <version>1.77</version>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>com.sun.media</groupId>
       <artifactId>jai-codec</artifactId>
diff --git a/fop-core/src/main/java/org/apache/fop/pdf/CMSProcessableInputStream.java b/fop-core/src/main/java/org/apache/fop/pdf/CMSProcessableInputStream.java
new file mode 100644 (file)
index 0000000..1d1343a
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 org.apache.fop.pdf;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
+import org.bouncycastle.cms.CMSTypedData;
+
+import org.apache.commons.io.IOUtils;
+
+/**
+ * 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.
+ */
+class CMSProcessableInputStream implements CMSTypedData {
+    private 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 {
+        // read the content only one time
+        IOUtils.copy(in, out);
+        in.close();
+    }
+
+    @Override
+    public ASN1ObjectIdentifier getContentType() {
+        return contentType;
+    }
+}
diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFSignParams.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFSignParams.java
new file mode 100644 (file)
index 0000000..4ad79f7
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+/* $Id$ */
+package org.apache.fop.pdf;
+
+public class PDFSignParams {
+    private String pkcs12;
+    private String name;
+    private String location;
+    private String reason;
+    private String password = "";
+
+    public PDFSignParams(String pkcs12, String name, String location, String reason, String password) {
+        this.pkcs12 = pkcs12;
+        this.name = name;
+        this.location = location;
+        this.reason = reason;
+        if (password != null) {
+            this.password = password;
+        }
+    }
+
+    public String getPkcs12() {
+        return pkcs12;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getLocation() {
+        return location;
+    }
+
+    public String getReason() {
+        return reason;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+}
diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java
new file mode 100644 (file)
index 0000000..199d5a9
--- /dev/null
@@ -0,0 +1,277 @@
+/*
+ * 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.
+ */
+
+/* $Id$ */
+package org.apache.fop.pdf;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Enumeration;
+
+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.OperatorException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.output.CountingOutputStream;
+
+import org.apache.xmlgraphics.io.TempResourceURIGenerator;
+
+import org.apache.fop.apps.FOUserAgent;
+
+public class PDFSignature {
+    private static final int SIZE_OF_CONTENTS = 18944;
+    private static final TempResourceURIGenerator TEMP_URI_GENERATOR = new TempResourceURIGenerator("pdfsign2");
+    private Perms perms;
+    private PDFRoot root;
+    private PrivateKey privateKey;
+    private long startOfDocMDP;
+    private long startOfContents;
+    private FOUserAgent userAgent;
+    private URI tempURI;
+    private PDFSignParams signParams;
+
+    static class TransformParams extends PDFDictionary {
+        TransformParams() {
+            put("Type", new PDFName("TransformParams"));
+            put("P", 2);
+            put("V", new PDFName("1.2"));
+        }
+    }
+
+    static class SigRef extends PDFDictionary {
+        SigRef() {
+            put("Type", new PDFName("SigRef"));
+            put("TransformMethod", new PDFName("DocMDP"));
+            put("DigestMethod", new PDFName("SHA1"));
+            put("TransformParams", new TransformParams());
+        }
+    }
+
+    class Contents extends PDFObject {
+        protected String toPDFString() {
+            return PDFText.toHex(new byte[SIZE_OF_CONTENTS / 2]);
+        }
+
+        public int output(OutputStream stream) throws IOException {
+            CountingOutputStream countingOutputStream = (CountingOutputStream) stream;
+            startOfContents = startOfDocMDP + countingOutputStream.getByteCount();
+            return super.output(stream);
+        }
+    }
+
+    class DocMDP extends PDFDictionary {
+        DocMDP() {
+            put("Type", new PDFName("Sig"));
+            put("Filter", new PDFName("Adobe.PPKLite"));
+            put("SubFilter", new PDFName("adbe.pkcs7.detached"));
+            if (signParams.getName() != null) {
+                put("Name", signParams.getName());
+            }
+            if (signParams.getLocation() != null) {
+                put("Location", signParams.getLocation());
+            }
+            if (signParams.getReason() != null) {
+                put("Reason", signParams.getReason());
+            }
+            put("M", PDFInfo.formatDateTime(new Date()));
+            PDFArray array = new PDFArray();
+            array.add(new SigRef());
+            put("Reference", array);
+            put("Contents", new Contents());
+            put("ByteRange", new PDFArray(0, 1000000000, 1000000000, 1000000000));
+        }
+
+        public int output(OutputStream stream) throws IOException {
+            if (stream instanceof CountingOutputStream) {
+                CountingOutputStream countingOutputStream = (CountingOutputStream) stream;
+                startOfDocMDP = countingOutputStream.getByteCount();
+                return super.output(stream);
+            }
+            throw new IOException("Disable pdf linearization");
+        }
+    }
+
+    static class Perms extends PDFDictionary {
+        DocMDP docMDP;
+        Perms(PDFRoot root, DocMDP docMDP) {
+            this.docMDP = docMDP;
+            root.getDocument().registerObject(docMDP);
+            put("DocMDP", docMDP);
+        }
+    }
+
+    static class SigField extends PDFDictionary {
+        SigField(Perms perms, PDFPage page, PDFRoot root) {
+            root.getDocument().registerObject(this);
+            put("FT", new PDFName("Sig"));
+            put("Type", new PDFName("Annot"));
+            put("Subtype", new PDFName("Widget"));
+            put("F", 132);
+            put("T", "Signature1");
+            put("Rect", new PDFRectangle(0, 0, 0, 0));
+            put("V", perms.docMDP);
+            put("P", new PDFReference(page));
+            put("AP", new AP(root));
+        }
+    }
+
+    static class AP extends PDFDictionary {
+        AP(PDFRoot root) {
+            put("N", new FormXObject(root));
+        }
+    }
+
+    static class FormXObject extends PDFStream {
+        FormXObject(PDFRoot root) {
+            root.getDocument().registerObject(this);
+            put("Length", 0);
+            put("Type", new PDFName("XObject"));
+            put("Subtype", new PDFName("Form"));
+            put("BBox", new PDFRectangle(0, 0, 0, 0));
+        }
+    }
+
+    static class AcroForm extends PDFDictionary {
+        AcroForm(SigField sigField) {
+            PDFArray fields = new PDFArray();
+            fields.add(sigField);
+            put("Fields", fields);
+            put("SigFlags", 3);
+        }
+    }
+
+    public PDFSignature(PDFRoot root, FOUserAgent userAgent, PDFSignParams signParams) {
+        this.root = root;
+        this.userAgent = userAgent;
+        this.signParams = signParams;
+        perms = new Perms(root, new DocMDP());
+        root.put("Perms", perms);
+        tempURI = TEMP_URI_GENERATOR.generate();
+    }
+
+    public void add(PDFPage page) {
+        SigField sigField = new SigField(perms, page, root);
+        root.put("AcroForm", new AcroForm(sigField));
+        page.addAnnotation(sigField);
+    }
+
+    public void signPDF(URI uri, OutputStream os) throws IOException {
+        try (InputStream pdfIS = getTempIS(uri)) {
+            pdfIS.mark(Integer.MAX_VALUE);
+            String byteRangeValues = "0 1000000000 1000000000 1000000000";
+            String byteRange = "\n  /ByteRange [" + byteRangeValues + "]";
+            int pdfLength = pdfIS.available();
+            long offsetToPDFEnd = startOfContents + SIZE_OF_CONTENTS + 2 + byteRange.length();
+            long endOfPDFSize = pdfLength - offsetToPDFEnd;
+            String byteRangeValues2 = String.format("0 %s %s %s", startOfContents,
+                    startOfContents + SIZE_OF_CONTENTS + 2, byteRange.length() + endOfPDFSize);
+            byteRange = "\n  /ByteRange [" + byteRangeValues2 + "]";
+            String byteRangePadding = new String(new char[byteRangeValues.length() - byteRangeValues2.length()])
+                    .replace("\0", " ");
+            try (OutputStream editedPDF = getTempOS()) {
+                IOUtils.copyLarge(pdfIS, editedPDF, 0, startOfContents);
+                editedPDF.write(byteRange.getBytes("UTF-8"));
+                editedPDF.write(byteRangePadding.getBytes("UTF-8"));
+                IOUtils.copyLarge(pdfIS, editedPDF, offsetToPDFEnd - startOfContents, Long.MAX_VALUE);
+            }
+            pdfIS.reset();
+            IOUtils.copyLarge(pdfIS, os, 0, startOfContents);
+            try (InputStream is = getTempIS(tempURI)) {
+                byte[] signed = readPKCS(is);
+                String signedHexPadding = new String(new char[SIZE_OF_CONTENTS - (signed.length * 2)])
+                        .replace("\0", "0");
+                String signedHex = "<" + PDFText.toHex(signed, false) + signedHexPadding + ">";
+                os.write(signedHex.getBytes("UTF-8"));
+            }
+            os.write(byteRange.getBytes("UTF-8"));
+            os.write(byteRangePadding.getBytes("UTF-8"));
+            IOUtils.copyLarge(pdfIS, os, offsetToPDFEnd - startOfContents, Long.MAX_VALUE);
+        }
+    }
+
+    private OutputStream getTempOS() throws IOException {
+        return new BufferedOutputStream(userAgent.getResourceResolver().getOutputStream(tempURI));
+    }
+
+    private InputStream getTempIS(URI uri) throws IOException {
+        return new BufferedInputStream(userAgent.getResourceResolver().getResource(uri));
+    }
+
+    private byte[] readPKCS(InputStream pdf) throws IOException {
+        try {
+            char[] password = signParams.getPassword().toCharArray();
+            KeyStore keystore = KeyStore.getInstance("PKCS12");
+            try (InputStream is = userAgent.getResourceResolver().getResource(signParams.getPkcs12())) {
+                keystore.load(is, password);
+            }
+            Certificate[] certificates = readKeystore(keystore, password);
+            return sign(pdf, certificates);
+        } catch (GeneralSecurityException | URISyntaxException | OperatorException | CMSException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private Certificate[] readKeystore(KeyStore keystore, char[] password)
+            throws GeneralSecurityException, IOException {
+        Enumeration<String> aliases = keystore.aliases();
+        while (aliases.hasMoreElements()) {
+            String alias = aliases.nextElement();
+            privateKey = (PrivateKey) keystore.getKey(alias, password);
+            Certificate[] certChain = keystore.getCertificateChain(alias);
+            if (certChain != null) {
+                Certificate cert = certChain[0];
+                if (cert instanceof X509Certificate) {
+                    ((X509Certificate) cert).checkValidity();
+                }
+                return certChain;
+            }
+        }
+        throw new IOException("Could not find certificate");
+    }
+
+    private byte[] sign(InputStream content, Certificate[] certChain)
+            throws GeneralSecurityException, OperatorException, CMSException, IOException {
+        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
+        X509Certificate cert = (X509Certificate) certChain[0];
+        ContentSigner sha2Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey);
+        gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
+                new JcaDigestCalculatorProviderBuilder().build()).build(sha2Signer, cert));
+        gen.addCertificates(new JcaCertStore(Arrays.asList(certChain)));
+        CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
+        CMSSignedData signedData = gen.generate(msg, false);
+        return signedData.getEncoded();
+    }
+}
index 4158d0f2a673974fcc68fdbb6a03d622e4a6fcd4..c81bbc50beabda287b5e9db280b92a7889883371 100644 (file)
@@ -24,14 +24,19 @@ import java.awt.Rectangle;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.Point2D;
 import java.awt.geom.Rectangle2D;
+import java.io.BufferedOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 
+import org.apache.commons.io.output.CountingOutputStream;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+import org.apache.xmlgraphics.io.TempResourceURIGenerator;
 import org.apache.xmlgraphics.xmp.Metadata;
 
 import org.apache.fop.accessibility.StructureTreeEventHandler;
@@ -43,6 +48,7 @@ import org.apache.fop.pdf.PDFDocument;
 import org.apache.fop.pdf.PDFPage;
 import org.apache.fop.pdf.PDFReference;
 import org.apache.fop.pdf.PDFResources;
+import org.apache.fop.pdf.PDFSignature;
 import org.apache.fop.pdf.PDFStream;
 import org.apache.fop.render.extensions.prepress.PageBoundaries;
 import org.apache.fop.render.extensions.prepress.PageScale;
@@ -63,6 +69,7 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
 
     /** logging instance */
     private static Log log = LogFactory.getLog(PDFDocumentHandler.class);
+    private static final TempResourceURIGenerator SIGN_TEMP_URI_GENERATOR = new TempResourceURIGenerator("pdfsign");
 
     private boolean accessEnabled;
 
@@ -100,6 +107,9 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
     private Map<String, Object> usedFieldNames = new HashMap<>();
     private Map<Integer, PDFArray> pageNumbers = new HashMap<Integer, PDFArray>();
     private Map<String, PDFReference> contents = new HashMap<String, PDFReference>();
+    private PDFSignature pdfSignature;
+    private URI signTempURI;
+    private OutputStream orgOutputStream;
 
     /**
      * Default constructor.
@@ -157,7 +167,8 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
     public void startDocument() throws IFException {
         super.startDocument();
         try {
-            this.pdfDoc = pdfUtil.setupPDFDocument(this.outputStream);
+            setupPDFSigning();
+            this.pdfDoc = pdfUtil.setupPDFDocument(outputStream);
             this.accessEnabled = getUserAgent().isAccessibilityEnabled();
             if (accessEnabled) {
                 setupAccessibility();
@@ -167,6 +178,15 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
         }
     }
 
+    private void setupPDFSigning() throws IOException {
+        if (pdfUtil.getSignParams() != null) {
+            orgOutputStream = outputStream;
+            signTempURI = SIGN_TEMP_URI_GENERATOR.generate();
+            outputStream = new BufferedOutputStream(getUserAgent().getResourceResolver().getOutputStream(signTempURI));
+            outputStream = new CountingOutputStream(outputStream);
+        }
+    }
+
     private void setupAccessibility() {
         pdfDoc.getRoot().makeTagged();
         logicalStructureHandler = new PDFLogicalStructureHandler(pdfDoc);
@@ -201,6 +221,18 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
             throw new IFException("I/O error in endDocument()", ioe);
         }
         super.endDocument();
+        signPDF();
+    }
+
+    private void signPDF() {
+        if (signTempURI != null) {
+            try {
+                outputStream.close();
+                pdfSignature.signPDF(signTempURI, orgOutputStream);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
     }
 
     /** {@inheritDoc} */
@@ -266,6 +298,11 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
         basicPageTransform.scale(scaleX, scaleY);
         generator.saveGraphicsState();
         generator.concatenate(basicPageTransform);
+
+        if (signTempURI != null && pdfSignature == null) {
+            pdfSignature = new PDFSignature(pdfDoc.getRoot(), getUserAgent(), pdfUtil.getSignParams());
+            pdfSignature.add(currentPage);
+        }
     }
 
     private Rectangle2D toPDFCoordSystem(Rectangle box, AffineTransform transform) {
index e71072bfe113ca896e97d671e570c45cbd05b546..075e6037e7078f4eb34276f4435b35771d2923bc 100644 (file)
@@ -38,6 +38,7 @@ import org.apache.fop.fonts.DefaultFontConfig.DefaultFontConfigParser;
 import org.apache.fop.fonts.FontEventAdapter;
 import org.apache.fop.pdf.PDFEncryptionParams;
 import org.apache.fop.pdf.PDFFilterList;
+import org.apache.fop.pdf.PDFSignParams;
 import org.apache.fop.render.RendererConfig;
 import org.apache.fop.render.RendererConfigOption;
 import org.apache.fop.util.LogUtil;
@@ -155,11 +156,28 @@ public final class PDFRendererConfig implements RendererConfig {
                 parseAndPut(LINEARIZATION, cfg);
                 parseAndPut(FORM_XOBJECT, cfg);
                 parseAndPut(VERSION, cfg);
+                configureSignParams(cfg);
             } catch (ConfigurationException e) {
                 LogUtil.handleException(LOG, e, strict);
             }
         }
 
+        private void configureSignParams(Configuration cfg) throws FOPException {
+            Configuration signCfd = cfg.getChild(PDFSignOption.SIGN_PARAMS, false);
+            if (signCfd != null) {
+                String keystore = parseConfig(signCfd, PDFSignOption.KEYSTORE);
+                if (keystore == null) {
+                    throw new FOPException("No keystore file defined inside sign-params");
+                }
+                String name = parseConfig(signCfd, PDFSignOption.NAME);
+                String location = parseConfig(signCfd, PDFSignOption.LOCATION);
+                String reason = parseConfig(signCfd, PDFSignOption.REASON);
+                String password = parseConfig(signCfd, PDFSignOption.PASSWORD);
+                PDFSignParams signParams = new PDFSignParams(keystore, name, location, reason, password);
+                configOptions.put(PDFRendererOption.SIGN_PARAMS, signParams);
+            }
+        }
+
         private void configureEncryptionParams(Configuration cfg, FOUserAgent userAgent, boolean strict) {
             Configuration encryptCfg = cfg.getChild(ENCRYPTION_PARAMS, false);
             if (encryptCfg != null) {
index 97834ed2a1666ceeb287ad298958fb83089ad280..f39d4e08bce02986bbb8f9e8fc9cc5e325cd4e12 100644 (file)
@@ -115,6 +115,12 @@ public enum PDFRendererOption implements RendererConfigOption {
                 throw new RuntimeException(e);
             }
         }
+    },
+    SIGN_PARAMS(PDFSignOption.SIGN_PARAMS, null) {
+        @Override
+        Object deserialize(String value) {
+            throw new UnsupportedOperationException();
+        }
     };
 
     private final String name;
index 0360782ef58d92de61bdc199925d294fa136532d..250857709a98a664ac26b8d5f1a8323436f80b15 100644 (file)
@@ -26,6 +26,7 @@ import java.util.Map;
 
 import org.apache.fop.pdf.PDFAMode;
 import org.apache.fop.pdf.PDFEncryptionParams;
+import org.apache.fop.pdf.PDFSignParams;
 import org.apache.fop.pdf.PDFUAMode;
 import org.apache.fop.pdf.PDFVTMode;
 import org.apache.fop.pdf.PDFXMode;
@@ -125,6 +126,10 @@ public final class PDFRendererOptionsConfig {
         return encryptionConfig;
     }
 
+    public PDFSignParams getSignParams() {
+        return (PDFSignParams) properties.get(PDFRendererOption.SIGN_PARAMS);
+    }
+
     public URI getOutputProfileURI() {
         return (URI) properties.get(OUTPUT_PROFILE);
     }
index 32fdc6e32c40b3a7cc942394449f3bbc8d98e182..8a6ebe5be702bd9f6e17d7ac85d0122331dfd070 100644 (file)
@@ -74,6 +74,7 @@ import org.apache.fop.pdf.PDFPage;
 import org.apache.fop.pdf.PDFPageLabels;
 import org.apache.fop.pdf.PDFReference;
 import org.apache.fop.pdf.PDFSetOCGStateAction;
+import org.apache.fop.pdf.PDFSignParams;
 import org.apache.fop.pdf.PDFTransitionAction;
 import org.apache.fop.pdf.PDFXMode;
 import org.apache.fop.pdf.Version;
@@ -638,6 +639,10 @@ class PDFRenderingUtil {
         return this.pdfDoc;
     }
 
+    public PDFSignParams getSignParams() {
+        return rendererConfig.getSignParams();
+    }
+
     /**
      * Generates a page label in the PDF document.
      * @param pageIndex the index of the page
diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFSignOption.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFSignOption.java
new file mode 100644 (file)
index 0000000..ade3c38
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+/* $Id$ */
+package org.apache.fop.render.pdf;
+
+import org.apache.fop.render.RendererConfigOption;
+
+public enum PDFSignOption implements RendererConfigOption {
+    KEYSTORE("keystore"),
+    NAME("name"),
+    LOCATION("location"),
+    REASON("reason"),
+    PASSWORD("password");
+    public static final String SIGN_PARAMS = "sign-params";
+    private final String name;
+    PDFSignOption(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+    public Object getDefaultValue() {
+        return null;
+    }
+}
diff --git a/fop-core/src/test/java/org/apache/fop/pdf/PDFSigningTestCase.java b/fop-core/src/test/java/org/apache/fop/pdf/PDFSigningTestCase.java
new file mode 100644 (file)
index 0000000..993487e
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+/* $Id$ */
+package org.apache.fop.pdf;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.util.StringTokenizer;
+
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.fop.apps.FOUserAgent;
+import org.apache.fop.apps.Fop;
+import org.apache.fop.apps.FopFactory;
+import org.apache.fop.apps.MimeConstants;
+import org.apache.fop.fo.pagination.LayoutMasterSetTestCase;
+
+public class PDFSigningTestCase {
+    @Test
+    public void textFO() throws Exception {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        foToOutput(out, MimeConstants.MIME_PDF);
+        StringTokenizer byteRange = new StringTokenizer(out.toString().split("/ByteRange ")[1]);
+        byteRange.nextToken();
+        int startOfContents = Integer.parseInt(byteRange.nextToken());
+        int endOfContents = Integer.parseInt(byteRange.nextToken());
+        int sizeOfEnd = Integer.parseInt(byteRange.nextToken().replace("]", ""));
+        int sizeOfContents = 18944;
+        Assert.assertEquals(endOfContents, startOfContents + sizeOfContents + 2);
+        Assert.assertEquals(out.size(), startOfContents + sizeOfContents + 2 + sizeOfEnd);
+        ByteArrayInputStream bis = new ByteArrayInputStream(out.toByteArray());
+        bis.skip(startOfContents);
+        Assert.assertEquals(bis.read(), '<');
+        bis.skip(sizeOfContents);
+        Assert.assertEquals(bis.read(), '>');
+        byte[] end = new byte[200];
+        bis.read(end);
+        String endStr = new String(end);
+        Assert.assertTrue(endStr.contains(
+                "/ByteRange [0 " + startOfContents + " " + endOfContents + " " + sizeOfEnd + "]"));
+        Assert.assertTrue(endStr.contains("/FT /Sig\n"
+                + "  /Type /Annot\n"
+                + "  /Subtype /Widget\n"
+                + "  /F 132\n"
+                + "  /T (Signature1)\n"
+                + "  /Rect [0 0 0 0]"));
+    }
+
+    private void foToOutput(ByteArrayOutputStream out, String mimeFopIf) throws Exception {
+        FopFactory fopFactory = getFopFactory();
+        FOUserAgent userAgent = fopFactory.newFOUserAgent();
+        Fop fop = fopFactory.newFop(mimeFopIf, userAgent, out);
+        Transformer transformer = TransformerFactory.newInstance().newTransformer();
+        Source src = new StreamSource(LayoutMasterSetTestCase.class.getResourceAsStream("side-regions.fo"));
+        Result res = new SAXResult(fop.getDefaultHandler());
+        transformer.transform(src, res);
+    }
+
+    private FopFactory getFopFactory() throws Exception {
+        String pkcs = PDFSigningTestCase.class.getResource("keystore.pkcs12").toString();
+        String fopxconf = "<fop version=\"1.0\">\n"
+                + "  <renderers>\n"
+                + "    <renderer mime=\"application/pdf\">\n"
+                + "    <sign-params>\n"
+                + "      <keystore>" + pkcs + "</keystore>\n"
+                + "    </sign-params>\n"
+                + "    </renderer>\n"
+                + "  </renderers>\n"
+                + "</fop>\n";
+        return FopFactory.newInstance(new File(".").toURI(),
+                new ByteArrayInputStream(fopxconf.getBytes()));
+    }
+}
diff --git a/fop-core/src/test/resources/org/apache/fop/pdf/keystore.pkcs12 b/fop-core/src/test/resources/org/apache/fop/pdf/keystore.pkcs12
new file mode 100644 (file)
index 0000000..658ebb7
Binary files /dev/null and b/fop-core/src/test/resources/org/apache/fop/pdf/keystore.pkcs12 differ