fix: pkcs12 signing
TODO: add PEM support and use page number
This commit is contained in:
parent
6a9ef7d538
commit
f433e8032f
6 changed files with 697 additions and 218 deletions
|
@ -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;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStream;
|
||||||
import java.security.KeyFactory;
|
import java.io.OutputStream;
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.PrivateKey;
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.UnrecoverableKeyException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Calendar;
|
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.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.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.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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
@ -68,6 +42,17 @@ public class CertSignController {
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
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")
|
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sign PDF with a Digital Certificate",
|
summary = "Sign PDF with a Digital Certificate",
|
||||||
|
@ -87,203 +72,71 @@ public class CertSignController {
|
||||||
String name = request.getName();
|
String name = request.getName();
|
||||||
Integer pageNumber = request.getPageNumber();
|
Integer pageNumber = request.getPageNumber();
|
||||||
|
|
||||||
PrivateKey privateKey = null;
|
if (certType == null) {
|
||||||
X509Certificate cert = null;
|
throw new IllegalArgumentException("Cert type must be provided");
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
InputStream ksInputStream = null;
|
||||||
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
|
|
||||||
logger.info("Successfully loaded the provided PDF");
|
|
||||||
SignatureOptions signatureOptions = new SignatureOptions();
|
|
||||||
|
|
||||||
// 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
|
// TODO: page number
|
||||||
if (showSignature != null && showSignature) {
|
|
||||||
PDPage page = document.getPage(pageNumber - 1);
|
|
||||||
|
|
||||||
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
|
KeyStore ks = getKeyStore(ksInputStream, password);
|
||||||
if (acroForm == null) {
|
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
||||||
acroForm = new PDAcroForm(document);
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
document.getDocumentCatalog().setAcroForm(acroForm);
|
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);
|
private static void sign(
|
||||||
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
|
byte[] input,
|
||||||
PDRectangle rect =
|
OutputStream output,
|
||||||
new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
|
CreateSignature instance,
|
||||||
widget.setRectangle(rect);
|
String name,
|
||||||
page.getAnnotations().add(widget);
|
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
|
doc.addSignature(signature, instance);
|
||||||
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
|
doc.saveIncremental(output);
|
||||||
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();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] parsePEM(byte[] content) throws IOException {
|
// private byte[] parsePEM(byte[] content) throws IOException {
|
||||||
PemReader pemReader =
|
// PemReader pemReader =
|
||||||
new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
|
// new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
|
||||||
return pemReader.readPemObject().getContent();
|
// return pemReader.readPemObject().getContent();
|
||||||
}
|
// }
|
||||||
|
|
||||||
private boolean isPEM(byte[] content) {
|
// private boolean isPEM(byte[] content) {
|
||||||
String contentStr = new String(content);
|
// String contentStr = new String(content);
|
||||||
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
|
// return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue