From 08d676ebd139a827fc2e6756d1ab542bce1a17b7 Mon Sep 17 00:00:00 2001 From: Simon Steiner Date: Wed, 7 Feb 2024 10:29:57 +0000 Subject: [PATCH] FOP-3166: Add option to sign PDF --- fop-core/pom.xml | 12 + .../fop/pdf/CMSProcessableInputStream.java | 63 ++++ .../org/apache/fop/pdf/PDFSignParams.java | 57 ++++ .../java/org/apache/fop/pdf/PDFSignature.java | 277 ++++++++++++++++++ .../fop/render/pdf/PDFDocumentHandler.java | 39 ++- .../fop/render/pdf/PDFRendererConfig.java | 18 ++ .../fop/render/pdf/PDFRendererOption.java | 6 + .../render/pdf/PDFRendererOptionsConfig.java | 5 + .../fop/render/pdf/PDFRenderingUtil.java | 5 + .../apache/fop/render/pdf/PDFSignOption.java | 41 +++ .../apache/fop/pdf/PDFSigningTestCase.java | 97 ++++++ .../org/apache/fop/pdf/keystore.pkcs12 | Bin 0 -> 4110 bytes 12 files changed, 619 insertions(+), 1 deletion(-) create mode 100644 fop-core/src/main/java/org/apache/fop/pdf/CMSProcessableInputStream.java create mode 100644 fop-core/src/main/java/org/apache/fop/pdf/PDFSignParams.java create mode 100644 fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java create mode 100644 fop-core/src/main/java/org/apache/fop/render/pdf/PDFSignOption.java create mode 100644 fop-core/src/test/java/org/apache/fop/pdf/PDFSigningTestCase.java create mode 100644 fop-core/src/test/resources/org/apache/fop/pdf/keystore.pkcs12 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 @@ -111,6 +111,18 @@ 1.1.3 provided + + org.bouncycastle + bcpkix-jdk15to18 + 1.77 + provided + + + org.bouncycastle + bcprov-jdk15to18 + 1.77 + provided + com.sun.media jai-codec 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 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 usedFieldNames = new HashMap<>(); private Map pageNumbers = new HashMap(); private Map contents = new HashMap(); + 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 = "\n" + + " \n" + + " \n" + + " \n" + + " " + pkcs + "\n" + + " \n" + + " \n" + + " \n" + + "\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 0000000000000000000000000000000000000000..658ebb75cc05ed72e8ae3987103dfae0232d7d1b GIT binary patch literal 4110 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 literal 0 HcmV?d00001 -- 2.39.5