diff options
author | Simon Steiner <ssteiner@apache.org> | 2024-02-07 10:29:57 +0000 |
---|---|---|
committer | Simon Steiner <ssteiner@apache.org> | 2024-02-07 10:29:57 +0000 |
commit | 08d676ebd139a827fc2e6756d1ab542bce1a17b7 (patch) | |
tree | a9442598ec0e6ec939e7e2039268efaed8c97783 | |
parent | 75e734cdbff92b34324bcca21f1c1408a8944168 (diff) | |
download | xmlgraphics-fop-08d676ebd139a827fc2e6756d1ab542bce1a17b7.tar.gz xmlgraphics-fop-08d676ebd139a827fc2e6756d1ab542bce1a17b7.zip |
FOP-3166: Add option to sign PDF
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 Binary files differnew file mode 100644 index 000000000..658ebb75c --- /dev/null +++ b/fop-core/src/test/resources/org/apache/fop/pdf/keystore.pkcs12 |