1
0
mirror of https://github.com/Stirling-Tools/Stirling-PDF.git synced 2024-06-03 06:10:11 +02:00
Stirling-PDF/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java
Anthony Stirling 5f771b7851 formatting
2023-12-30 19:11:27 +00:00

290 lines
13 KiB
Java

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.security.KeyStore;
import java.security.PrivateKey;
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.util.Calendar;
import java.util.Collections;
import java.util.Date;
import org.apache.commons.io.IOUtils;
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;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
public class CertSignController {
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
static {
Security.addProvider(new BouncyCastleProvider());
}
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@Operation(
summary = "Sign PDF with a Digital Certificate",
description =
"This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO")
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
throws Exception {
MultipartFile pdf = request.getFileInput();
String certType = request.getCertType();
MultipartFile privateKeyFile = request.getPrivateKeyFile();
MultipartFile certFile = request.getCertFile();
MultipartFile p12File = request.getP12File();
String password = request.getPassword();
Boolean showSignature = request.isShowSignature();
String reason = request.getReason();
String location = request.getLocation();
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;
}
}
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();
// If you want to show the signature
// ATTEMPT 2
if (showSignature != null && showSignature) {
PDPage page = document.getPage(pageNumber - 1);
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
if (acroForm == null) {
acroForm = new PDAcroForm(document);
document.getDocumentCatalog().setAcroForm(acroForm);
}
// Create a new signature field and widget
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);
// 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();
}
} 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 boolean isPEM(byte[] content) {
String contentStr = new String(content);
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
}
}