fix: pkcs12 signing
TODO: add PEM support and use page number
This commit is contained in:
@@ -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");
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user