mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-11-01 01:21:18 +01:00 
			
		
		
		
	fix: pkcs12 signing
TODO: add PEM support and use page number
This commit is contained in:
		
							parent
							
								
									6a9ef7d538
								
							
						
					
					
						commit
						f433e8032f
					
				@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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<String> 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.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>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.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>This method is for internal use only.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>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 <a href="https://stackoverflow.com/questions/41767351">this
 | 
			
		||||
     * answer</a> or <a href="https://stackoverflow.com/questions/56867465">this answer</a>.
 | 
			
		||||
     *
 | 
			
		||||
     * @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.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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<SignerInformation> 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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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");
 | 
			
		||||
    // }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user