aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Steiner <ssteiner@apache.org>2024-02-07 10:29:57 +0000
committerSimon Steiner <ssteiner@apache.org>2024-02-07 10:29:57 +0000
commit08d676ebd139a827fc2e6756d1ab542bce1a17b7 (patch)
treea9442598ec0e6ec939e7e2039268efaed8c97783
parent75e734cdbff92b34324bcca21f1c1408a8944168 (diff)
downloadxmlgraphics-fop-08d676ebd139a827fc2e6756d1ab542bce1a17b7.tar.gz
xmlgraphics-fop-08d676ebd139a827fc2e6756d1ab542bce1a17b7.zip
FOP-3166: Add option to sign PDF
-rw-r--r--fop-core/pom.xml12
-rw-r--r--fop-core/src/main/java/org/apache/fop/pdf/CMSProcessableInputStream.java63
-rw-r--r--fop-core/src/main/java/org/apache/fop/pdf/PDFSignParams.java57
-rw-r--r--fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java277
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/pdf/PDFDocumentHandler.java39
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java18
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java6
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java5
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java5
-rw-r--r--fop-core/src/main/java/org/apache/fop/render/pdf/PDFSignOption.java41
-rw-r--r--fop-core/src/test/java/org/apache/fop/pdf/PDFSigningTestCase.java97
-rw-r--r--fop-core/src/test/resources/org/apache/fop/pdf/keystore.pkcs12bin0 -> 4110 bytes
12 files changed, 619 insertions, 1 deletions
diff --git a/fop-core/pom.xml b/fop-core/pom.xml
index bc2c56781..d1e1a2f65 100644
--- a/fop-core/pom.xml
+++ b/fop-core/pom.xml
@@ -112,6 +112,18 @@
<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>
<version>1.1.3</version>
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
index 000000000..1d1343a5e
--- /dev/null
+++ b/fop-core/src/main/java/org/apache/fop/pdf/CMSProcessableInputStream.java
@@ -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
index 000000000..4ad79f780
--- /dev/null
+++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFSignParams.java
@@ -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
index 000000000..199d5a9be
--- /dev/null
+++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java
@@ -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();
+ }
+}
diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFDocumentHandler.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFDocumentHandler.java
index 4158d0f2a..c81bbc50b 100644
--- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFDocumentHandler.java
+++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFDocumentHandler.java
@@ -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) {
diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java
index e71072bfe..075e6037e 100644
--- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java
+++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java
@@ -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) {
diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java
index 97834ed2a..f39d4e08b 100644
--- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java
+++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java
@@ -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;
diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java
index 0360782ef..250857709 100644
--- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java
+++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java
@@ -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);
}
diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java
index 32fdc6e32..8a6ebe5be 100644
--- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java
+++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java
@@ -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
index 000000000..ade3c38d3
--- /dev/null
+++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFSignOption.java
@@ -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
index 000000000..993487ed6
--- /dev/null
+++ b/fop-core/src/test/java/org/apache/fop/pdf/PDFSigningTestCase.java
@@ -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
index 000000000..658ebb75c
--- /dev/null
+++ b/fop-core/src/test/resources/org/apache/fop/pdf/keystore.pkcs12
Binary files differ