From f433e8032f8ec5ffabe0ec4c6b577a1341ab554c Mon Sep 17 00:00:00 2001 From: sbplat <71648843+sbplat@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:49:08 -0500 Subject: [PATCH] fix: pkcs12 signing TODO: add PEM support and use page number --- .../signature/CMSProcessableInputStream.java | 64 ++++ .../signature/CreateSignatureBase.java | 170 +++++++++++ .../pdfbox/examples/signature/TSAClient.java | 176 +++++++++++ .../signature/ValidationTimeStamp.java | 134 ++++++++ .../examples/util/ConnectedInputStream.java | 82 +++++ .../api/security/CertSignController.java | 289 +++++------------- 6 files changed, 697 insertions(+), 218 deletions(-) create mode 100644 src/main/java/org/apache/pdfbox/examples/signature/CMSProcessableInputStream.java create mode 100644 src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java create mode 100644 src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java create mode 100644 src/main/java/org/apache/pdfbox/examples/signature/ValidationTimeStamp.java create mode 100644 src/main/java/org/apache/pdfbox/examples/util/ConnectedInputStream.java diff --git a/src/main/java/org/apache/pdfbox/examples/signature/CMSProcessableInputStream.java b/src/main/java/org/apache/pdfbox/examples/signature/CMSProcessableInputStream.java new file mode 100644 index 00000000..48d1da98 --- /dev/null +++ b/src/main/java/org/apache/pdfbox/examples/signature/CMSProcessableInputStream.java @@ -0,0 +1,64 @@ +/* + * 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.pdfbox.examples.signature; + +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.CMSException; +import org.bouncycastle.cms.CMSTypedData; + +/** + * Wraps a InputStream into a CMSProcessable object for bouncy castle. It's a memory saving + * alternative to the {@link org.bouncycastle.cms.CMSProcessableByteArray CMSProcessableByteArray} + * class. + * + * @author Thomas Chojecki + */ +class CMSProcessableInputStream implements CMSTypedData { + private final InputStream in; + private final ASN1ObjectIdentifier contentType; + + CMSProcessableInputStream(InputStream is) { + this(new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()), is); + } + + CMSProcessableInputStream(ASN1ObjectIdentifier type, InputStream is) { + contentType = type; + in = is; + } + + @Override + public Object getContent() { + return in; + } + + @Override + public void write(OutputStream out) throws IOException, CMSException { + // read the content only one time + in.transferTo(out); + in.close(); + } + + @Override + public ASN1ObjectIdentifier getContentType() { + return contentType; + } +} diff --git a/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java b/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java new file mode 100644 index 00000000..646561f0 --- /dev/null +++ b/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java @@ -0,0 +1,170 @@ +/* + * Copyright 2015 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.pdfbox.examples.signature; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Enumeration; + +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +public abstract class CreateSignatureBase implements SignatureInterface { + private PrivateKey privateKey; + private Certificate[] certificateChain; + private String tsaUrl; + private boolean externalSigning; + + /** + * Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the + * signature. + * + * @param keystore is a pkcs12 keystore. + * @param pin is the pin for the keystore / private key + * @throws KeyStoreException if the keystore has not been initialized (loaded) + * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found + * @throws UnrecoverableKeyException if the given password is wrong + * @throws CertificateException if the certificate is not valid as signing time + * @throws IOException if no certificate could be found + */ + public CreateSignatureBase(KeyStore keystore, char[] pin) + throws KeyStoreException, + UnrecoverableKeyException, + NoSuchAlgorithmException, + IOException, + CertificateException { + // grabs the first alias from the keystore and get the private key. An + // alternative method or constructor could be used for setting a specific + // alias that should be used. + Enumeration aliases = keystore.aliases(); + String alias; + Certificate cert = null; + while (cert == null && aliases.hasMoreElements()) { + alias = aliases.nextElement(); + setPrivateKey((PrivateKey) keystore.getKey(alias, pin)); + Certificate[] certChain = keystore.getCertificateChain(alias); + if (certChain != null) { + setCertificateChain(certChain); + cert = certChain[0]; + if (cert instanceof X509Certificate) { + // avoid expired certificate + ((X509Certificate) cert).checkValidity(); + + //// SigUtils.checkCertificateUsage((X509Certificate) cert); + } + } + } + + if (cert == null) { + throw new IOException("Could not find certificate"); + } + } + + public final void setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + } + + public final void setCertificateChain(final Certificate[] certificateChain) { + this.certificateChain = certificateChain; + } + + public Certificate[] getCertificateChain() { + return certificateChain; + } + + public void setTsaUrl(String tsaUrl) { + this.tsaUrl = tsaUrl; + } + + /** + * SignatureInterface sample implementation. + * + *

This method will be called from inside of the pdfbox and create the PKCS #7 signature. The + * given InputStream contains the bytes that are given by the byte range. + * + *

This method is for internal use only. + * + *

Use your favorite cryptographic library to implement PKCS #7 signature creation. If you + * want to create the hash and the signature separately (e.g. to transfer only the hash to an + * external application), read this + * answer or this answer. + * + * @throws IOException + */ + @Override + public byte[] sign(InputStream content) throws IOException { + // cannot be done private (interface) + try { + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + X509Certificate cert = (X509Certificate) certificateChain[0]; + ContentSigner sha1Signer = + new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey); + gen.addSignerInfoGenerator( + new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().build()) + .build(sha1Signer, cert)); + gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain))); + CMSProcessableInputStream msg = new CMSProcessableInputStream(content); + CMSSignedData signedData = gen.generate(msg, false); + if (tsaUrl != null && !tsaUrl.isEmpty()) { + ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl); + signedData = validation.addSignedTimeStamp(signedData); + } + return signedData.getEncoded(); + } catch (GeneralSecurityException + | CMSException + | OperatorCreationException + | URISyntaxException e) { + throw new IOException(e); + } + } + + /** + * Set if external signing scenario should be used. If {@code false}, SignatureInterface would + * be used for signing. + * + *

Default: {@code false} + * + * @param externalSigning {@code true} if external signing should be performed + */ + public void setExternalSigning(boolean externalSigning) { + this.externalSigning = externalSigning; + } + + public boolean isExternalSigning() { + return externalSigning; + } +} diff --git a/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java b/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java new file mode 100644 index 00000000..a215fe52 --- /dev/null +++ b/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java @@ -0,0 +1,176 @@ +/* + * 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.pdfbox.examples.signature; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Random; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder; +import org.bouncycastle.tsp.TSPException; +import org.bouncycastle.tsp.TimeStampRequest; +import org.bouncycastle.tsp.TimeStampRequestGenerator; +import org.bouncycastle.tsp.TimeStampResponse; +import org.bouncycastle.tsp.TimeStampToken; + +/** + * Time Stamping Authority (TSA) Client [RFC 3161]. + * + * @author Vakhtang Koroghlishvili + * @author John Hewson + */ +public class TSAClient { + private static final Logger LOG = LogManager.getLogger(TSAClient.class); + + private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER = + new DefaultDigestAlgorithmIdentifierFinder(); + + private final URL url; + private final String username; + private final String password; + private final MessageDigest digest; + + // SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux + private static final Random RANDOM = new SecureRandom(); + + /** + * @param url the URL of the TSA service + * @param username user name of TSA + * @param password password of TSA + * @param digest the message digest to use + */ + public TSAClient(URL url, String username, String password, MessageDigest digest) { + this.url = url; + this.username = username; + this.password = password; + this.digest = digest; + } + + /** + * @param content + * @return the time stamp token + * @throws IOException if there was an error with the connection or data from the TSA server, or + * if the time stamp response could not be validated + */ + public TimeStampToken getTimeStampToken(InputStream content) throws IOException { + digest.reset(); + DigestInputStream dis = new DigestInputStream(content, digest); + while (dis.read() != -1) { + // do nothing + } + byte[] hash = digest.digest(); + + // 32-bit cryptographic nonce + int nonce = RANDOM.nextInt(); + + // generate TSA request + TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator(); + tsaGenerator.setCertReq(true); + ASN1ObjectIdentifier oid = ALGORITHM_OID_FINDER.find(digest.getAlgorithm()).getAlgorithm(); + TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce)); + + // get TSA response + byte[] tsaResponse = getTSAResponse(request.getEncoded()); + + TimeStampResponse response; + try { + response = new TimeStampResponse(tsaResponse); + response.validate(request); + } catch (TSPException e) { + throw new IOException(e); + } + + TimeStampToken timeStampToken = response.getTimeStampToken(); + if (timeStampToken == null) { + // https://www.ietf.org/rfc/rfc3161.html#section-2.4.2 + throw new IOException( + "Response from " + + url + + " does not have a time stamp token, status: " + + response.getStatus() + + " (" + + response.getStatusString() + + ")"); + } + + return timeStampToken; + } + + // gets response data for the given encoded TimeStampRequest data + // throws IOException if a connection to the TSA cannot be established + private byte[] getTSAResponse(byte[] request) throws IOException { + LOG.debug("Opening connection to TSA server"); + + // todo: support proxy servers + URLConnection connection = url.openConnection(); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setRequestProperty("Content-Type", "application/timestamp-query"); + + LOG.debug("Established connection to TSA server"); + + if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) { + String contentEncoding = connection.getContentEncoding(); + if (contentEncoding == null) { + contentEncoding = StandardCharsets.UTF_8.name(); + } + connection.setRequestProperty( + "Authorization", + "Basic " + + new String( + Base64.getEncoder() + .encode( + (username + ":" + password) + .getBytes(contentEncoding)))); + } + + // read response + try (OutputStream output = connection.getOutputStream()) { + output.write(request); + } catch (IOException ex) { + LOG.error("Exception when writing to {}", this.url, ex); + throw ex; + } + + LOG.debug("Waiting for response from TSA server"); + + byte[] response; + try (InputStream input = connection.getInputStream()) { + response = input.readAllBytes(); + } catch (IOException ex) { + LOG.error("Exception when reading from {}", this.url, ex); + throw ex; + } + + LOG.debug("Received response from TSA server"); + + return response; + } +} diff --git a/src/main/java/org/apache/pdfbox/examples/signature/ValidationTimeStamp.java b/src/main/java/org/apache/pdfbox/examples/signature/ValidationTimeStamp.java new file mode 100644 index 00000000..d01666d7 --- /dev/null +++ b/src/main/java/org/apache/pdfbox/examples/signature/ValidationTimeStamp.java @@ -0,0 +1,134 @@ +/* + * 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.pdfbox.examples.signature; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DERSet; +import org.bouncycastle.asn1.cms.Attribute; +import org.bouncycastle.asn1.cms.AttributeTable; +import org.bouncycastle.asn1.cms.Attributes; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.tsp.TimeStampToken; + +/** + * This class wraps the TSAClient and the work that has to be done with it. Like Adding Signed + * TimeStamps to a signature, or creating a CMS timestamp attribute (with a signed timestamp) + * + * @author Others + * @author Alexis Suter + */ +public class ValidationTimeStamp { + private TSAClient tsaClient; + + /** + * @param tsaUrl The url where TS-Request will be done. + * @throws NoSuchAlgorithmException + * @throws MalformedURLException + * @throws java.net.URISyntaxException + */ + public ValidationTimeStamp(String tsaUrl) + throws NoSuchAlgorithmException, MalformedURLException, URISyntaxException { + if (tsaUrl != null) { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + this.tsaClient = new TSAClient(new URI(tsaUrl).toURL(), null, null, digest); + } + } + + /** + * Creates a signed timestamp token by the given input stream. + * + * @param content InputStream of the content to sign + * @return the byte[] of the timestamp token + * @throws IOException + */ + public byte[] getTimeStampToken(InputStream content) throws IOException { + TimeStampToken timeStampToken = tsaClient.getTimeStampToken(content); + return timeStampToken.getEncoded(); + } + + /** + * Extend cms signed data with TimeStamp first or to all signers + * + * @param signedData Generated CMS signed data + * @return CMSSignedData Extended CMS signed data + * @throws IOException + */ + public CMSSignedData addSignedTimeStamp(CMSSignedData signedData) throws IOException { + SignerInformationStore signerStore = signedData.getSignerInfos(); + List newSigners = new ArrayList<>(); + + for (SignerInformation signer : signerStore.getSigners()) { + // This adds a timestamp to every signer (into his unsigned attributes) in the + // signature. + newSigners.add(signTimeStamp(signer)); + } + + // Because new SignerInformation is created, new SignerInfoStore has to be created + // and also be replaced in signedData. Which creates a new signedData object. + return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners)); + } + + /** + * Extend CMS Signer Information with the TimeStampToken into the unsigned Attributes. + * + * @param signer information about signer + * @return information about SignerInformation + * @throws IOException + */ + private SignerInformation signTimeStamp(SignerInformation signer) throws IOException { + AttributeTable unsignedAttributes = signer.getUnsignedAttributes(); + + ASN1EncodableVector vector = new ASN1EncodableVector(); + if (unsignedAttributes != null) { + vector = unsignedAttributes.toASN1EncodableVector(); + } + + TimeStampToken timeStampToken = + tsaClient.getTimeStampToken(new ByteArrayInputStream(signer.getSignature())); + byte[] token = timeStampToken.getEncoded(); + ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken; + ASN1Encodable signatureTimeStamp = + new Attribute(oid, new DERSet(ASN1Primitive.fromByteArray(token))); + + vector.add(signatureTimeStamp); + Attributes signedAttributes = new Attributes(vector); + + // There is no other way changing the unsigned attributes of the signer information. + // result is never null, new SignerInformation always returned, + // see source code of replaceUnsignedAttributes + return SignerInformation.replaceUnsignedAttributes( + signer, new AttributeTable(signedAttributes)); + } +} diff --git a/src/main/java/org/apache/pdfbox/examples/util/ConnectedInputStream.java b/src/main/java/org/apache/pdfbox/examples/util/ConnectedInputStream.java new file mode 100644 index 00000000..30434f71 --- /dev/null +++ b/src/main/java/org/apache/pdfbox/examples/util/ConnectedInputStream.java @@ -0,0 +1,82 @@ +/* + * 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.pdfbox.examples.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; + +/** + * Delegate class to close the connection when the class gets closed. + * + * @author Tilman Hausherr + */ +public class ConnectedInputStream extends InputStream { + HttpURLConnection con; + InputStream is; + + public ConnectedInputStream(HttpURLConnection con, InputStream is) { + this.con = con; + this.is = is; + } + + @Override + public int read() throws IOException { + return is.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return is.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return is.read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return is.skip(n); + } + + @Override + public int available() throws IOException { + return is.available(); + } + + @Override + public synchronized void mark(int readlimit) { + is.mark(readlimit); + } + + @Override + public synchronized void reset() throws IOException { + is.reset(); + } + + @Override + public boolean markSupported() { + return is.markSupported(); + } + + @Override + public void close() throws IOException { + is.close(); + con.disconnect(); + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 8990c789..c39eddd2 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -1,47 +1,21 @@ package stirling.software.SPDF.controller.api.security; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.security.KeyFactory; +import java.io.InputStream; +import java.io.OutputStream; import java.security.KeyStore; -import java.security.PrivateKey; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.security.Security; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.security.spec.PKCS8EncodedKeySpec; -import java.text.SimpleDateFormat; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import org.apache.commons.io.IOUtils; +import org.apache.pdfbox.examples.signature.CreateSignatureBase; import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.PDResources; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.font.PDType1Font; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; -import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; -import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; -import org.bouncycastle.cert.jcajce.JcaCertStore; -import org.bouncycastle.cms.CMSProcessableByteArray; -import org.bouncycastle.cms.CMSSignedData; -import org.bouncycastle.cms.CMSSignedDataGenerator; -import org.bouncycastle.cms.CMSTypedData; -import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; -import org.bouncycastle.util.io.pem.PemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -68,6 +42,17 @@ public class CertSignController { Security.addProvider(new BouncyCastleProvider()); } + class CreateSignature extends CreateSignatureBase { + public CreateSignature(KeyStore keystore, char[] pin) + throws KeyStoreException, + UnrecoverableKeyException, + NoSuchAlgorithmException, + IOException, + CertificateException { + super(keystore, pin); + } + } + @PostMapping(consumes = "multipart/form-data", value = "/cert-sign") @Operation( summary = "Sign PDF with a Digital Certificate", @@ -87,203 +72,71 @@ public class CertSignController { String name = request.getName(); Integer pageNumber = request.getPageNumber(); - PrivateKey privateKey = null; - X509Certificate cert = null; - - if (certType != null) { - logger.info("Cert type provided: {}", certType); - switch (certType) { - case "PKCS12": - if (p12File != null) { - KeyStore ks = KeyStore.getInstance("PKCS12"); - ks.load( - new ByteArrayInputStream(p12File.getBytes()), - password.toCharArray()); - String alias = ks.aliases().nextElement(); - if (!ks.isKeyEntry(alias)) { - throw new IllegalArgumentException( - "The provided PKCS12 file does not contain a private key."); - } - privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); - cert = (X509Certificate) ks.getCertificate(alias); - } - break; - case "PEM": - if (privateKeyFile != null && certFile != null) { - // Load private key - KeyFactory keyFactory = - KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); - if (isPEM(privateKeyFile.getBytes())) { - privateKey = - keyFactory.generatePrivate( - new PKCS8EncodedKeySpec( - parsePEM(privateKeyFile.getBytes()))); - } else { - privateKey = - keyFactory.generatePrivate( - new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); - } - - // Load certificate - CertificateFactory certFactory = - CertificateFactory.getInstance( - "X.509", BouncyCastleProvider.PROVIDER_NAME); - if (isPEM(certFile.getBytes())) { - cert = - (X509Certificate) - certFactory.generateCertificate( - new ByteArrayInputStream( - parsePEM(certFile.getBytes()))); - } else { - cert = - (X509Certificate) - certFactory.generateCertificate( - new ByteArrayInputStream(certFile.getBytes())); - } - } - break; - } + if (certType == null) { + throw new IllegalArgumentException("Cert type must be provided"); } - PDSignature signature = new PDSignature(); - signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter - signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1); - signature.setName(name); - signature.setLocation(location); - signature.setReason(reason); - signature.setSignDate(Calendar.getInstance()); - // Load the PDF - try (PDDocument document = PDDocument.load(pdf.getBytes())) { - logger.info("Successfully loaded the provided PDF"); - SignatureOptions signatureOptions = new SignatureOptions(); + InputStream ksInputStream = null; - // If you want to show the signature + switch (certType) { + case "PKCS12": + ksInputStream = p12File.getInputStream(); + break; + case "PEM": + throw new IllegalArgumentException("TODO: PEM not supported yet"); + // ksInputStream = privateKeyFile.getInputStream(); + // break; + default: + throw new IllegalArgumentException("Invalid cert type: " + certType); + } - // ATTEMPT 2 - if (showSignature != null && showSignature) { - PDPage page = document.getPage(pageNumber - 1); + // TODO: page number - PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); - if (acroForm == null) { - acroForm = new PDAcroForm(document); - document.getDocumentCatalog().setAcroForm(acroForm); - } + KeyStore ks = getKeyStore(ksInputStream, password); + CreateSignature createSignature = new CreateSignature(ks, password.toCharArray()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + sign(pdf.getBytes(), baos, createSignature, name, location, reason); + return WebResponseUtils.boasToWebResponse( + baos, pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf"); + } - // Create a new signature field and widget + private static KeyStore getKeyStore(InputStream is, String password) throws Exception { + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(is, password.toCharArray()); + return ks; + } - PDSignatureField signatureField = new PDSignatureField(acroForm); - PDAnnotationWidget widget = signatureField.getWidgets().get(0); - PDRectangle rect = - new PDRectangle(100, 100, 200, 50); // Define the rectangle size here - widget.setRectangle(rect); - page.getAnnotations().add(widget); + private static void sign( + byte[] input, + OutputStream output, + CreateSignature instance, + String name, + String location, + String reason) { + try (PDDocument doc = PDDocument.load(input)) { + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); + signature.setName(name); + signature.setLocation(location); + signature.setReason(reason); + signature.setSignDate(Calendar.getInstance()); - // Set the appearance for the signature field - PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); - PDAppearanceStream appearanceStream = new PDAppearanceStream(document); - appearanceStream.setResources(new PDResources()); - appearanceStream.setBBox(rect); - appearanceDict.setNormalAppearance(appearanceStream); - widget.setAppearance(appearanceDict); - - try (PDPageContentStream contentStream = - new PDPageContentStream(document, appearanceStream)) { - contentStream.beginText(); - contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); - contentStream.newLineAtOffset(110, 130); - contentStream.showText( - "Digitally signed by: " + (name != null ? name : "Unknown")); - contentStream.newLineAtOffset(0, -15); - contentStream.showText( - "Date: " - + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z") - .format(new Date())); - contentStream.newLineAtOffset(0, -15); - if (reason != null && !reason.isEmpty()) { - contentStream.showText("Reason: " + reason); - contentStream.newLineAtOffset(0, -15); - } - if (location != null && !location.isEmpty()) { - contentStream.showText("Location: " + location); - contentStream.newLineAtOffset(0, -15); - } - contentStream.endText(); - } - - // Add the widget annotation to the page - page.getAnnotations().add(widget); - - // Add the signature field to the acroform - acroForm.getFields().add(signatureField); - - // Handle multiple signatures by ensuring a unique field name - String baseFieldName = "Signature"; - String signatureFieldName = baseFieldName; - int suffix = 1; - while (acroForm.getField(signatureFieldName) != null) { - suffix++; - signatureFieldName = baseFieldName + suffix; - } - signatureField.setPartialName(signatureFieldName); - } - - document.addSignature(signature, signatureOptions); - logger.info("Signature added to the PDF document"); - // External signing - ExternalSigningSupport externalSigning = - document.saveIncrementalForExternalSigning(new ByteArrayOutputStream()); - - byte[] content = IOUtils.toByteArray(externalSigning.getContent()); - - // Using BouncyCastle to sign - CMSTypedData cmsData = new CMSProcessableByteArray(content); - - CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); - ContentSigner signer = - new JcaContentSignerBuilder("SHA256withRSA") - .setProvider(BouncyCastleProvider.PROVIDER_NAME) - .build(privateKey); - - gen.addSignerInfoGenerator( - new JcaSignerInfoGeneratorBuilder( - new JcaDigestCalculatorProviderBuilder() - .setProvider(BouncyCastleProvider.PROVIDER_NAME) - .build()) - .build(signer, cert)); - - gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); - CMSSignedData signedData = gen.generate(cmsData, false); - - byte[] cmsSignature = signedData.getEncoded(); - logger.info("About to sign content using BouncyCastle"); - externalSigning.setSignature(cmsSignature); - logger.info("Signature set successfully"); - - // After setting the signature, return the resultant PDF - try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { - document.save(signedPdfOutput); - return WebResponseUtils.boasToWebResponse( - signedPdfOutput, - pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf"); - - } catch (Exception e) { - e.printStackTrace(); - } + doc.addSignature(signature, instance); + doc.saveIncremental(output); } catch (Exception e) { e.printStackTrace(); } - - return null; } - private byte[] parsePEM(byte[] content) throws IOException { - PemReader pemReader = - new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); - return pemReader.readPemObject().getContent(); - } + // private byte[] parsePEM(byte[] content) throws IOException { + // PemReader pemReader = + // new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); + // return pemReader.readPemObject().getContent(); + // } - private boolean isPEM(byte[] content) { - String contentStr = new String(content); - return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); - } + // private boolean isPEM(byte[] content) { + // String contentStr = new String(content); + // return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); + // } }