complete itext removal

This commit is contained in:
Anthony Stirling 2023-09-03 01:23:44 +01:00
parent 9ece6dacbd
commit 18172aa33a
8 changed files with 226 additions and 500 deletions

View file

@ -66,7 +66,6 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h
## Technologies used ## Technologies used
- Spring Boot + Thymeleaf - Spring Boot + Thymeleaf
- PDFBox - PDFBox
- IText7
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions - [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF) - [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
- HTML, CSS, JavaScript - HTML, CSS, JavaScript

View file

@ -89,7 +89,6 @@ dependencies {
implementation 'org.apache.pdfbox:xmpbox:2.0.29' implementation 'org.apache.pdfbox:xmpbox:2.0.29'
implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
implementation 'com.itextpdf:itext7-core:7.2.5'
implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-core' implementation 'io.micrometer:micrometer-core'
implementation group: 'com.google.zxing', name: 'core', version: '3.5.1' implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'

View file

@ -15,13 +15,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;

View file

@ -71,7 +71,7 @@ public class ConvertEpubToPdf {
// Assuming a pseudo-code function that merges multiple PDFs into one. // Assuming a pseudo-code function that merges multiple PDFs into one.
private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) { private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) {
// You can use a library such as iText or PDFBox to perform the merging here. // You can use a library such as PDFBox to perform the merging here.
// Return the byte[] of the merged PDF. // Return the byte[] of the merged PDF.
return null; return null;
} }

View file

@ -34,7 +34,7 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.source.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;

View file

@ -1,6 +1,7 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -18,7 +19,7 @@ import java.text.SimpleDateFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.io.pem.PemReader; import org.bouncycastle.util.io.pem.PemReader;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -30,267 +31,273 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.font.constants.StandardFonts; import org.bouncycastle.cert.jcajce.JcaCertStore;
import com.itextpdf.kernel.font.PdfFont; import org.bouncycastle.cms.CMSException;
import com.itextpdf.kernel.font.PdfFontFactory; import org.bouncycastle.cms.CMSProcessableByteArray;
import com.itextpdf.kernel.geom.Rectangle; import org.bouncycastle.cms.CMSSignedData;
import com.itextpdf.kernel.pdf.PdfDocument; import org.bouncycastle.cms.CMSSignedDataGenerator;
import com.itextpdf.kernel.pdf.PdfPage; import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import com.itextpdf.kernel.pdf.PdfReader; import org.bouncycastle.operator.ContentSigner;
import com.itextpdf.kernel.pdf.StampingProperties; import org.bouncycastle.operator.OperatorCreationException;
import com.itextpdf.signatures.BouncyCastleDigest; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import com.itextpdf.signatures.DigestAlgorithms; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import com.itextpdf.signatures.IExternalDigest; import org.bouncycastle.cms.CMSTypedData;
import com.itextpdf.signatures.IExternalSignature; import java.io.ByteArrayInputStream;
import com.itextpdf.signatures.PdfPKCS7; import java.io.ByteArrayOutputStream;
import com.itextpdf.signatures.PdfSignatureAppearance; import java.io.IOException;
import com.itextpdf.signatures.PdfSigner; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import com.itextpdf.signatures.PrivateKeySignature; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import com.itextpdf.signatures.SignatureUtil; import org.apache.pdfbox.pdmodel.PDResources;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ContentDisposition;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.http.ResponseEntity;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
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.digitalsignature.ExternalSigningSupport;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@RestController @RestController
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class CertSignController { public class CertSignController {
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
static { static {
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
} }
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign") @PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@Operation(summary = "Sign PDF with a Digital Certificate", @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")
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[]> signPDF2(
public ResponseEntity<byte[]> signPDF( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be signed") MultipartFile pdf,
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be signed")
MultipartFile pdf,
@RequestParam(value = "certType", required = false) @RequestParam(value = "certType", required = false) @Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {
@Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {"PKCS12", "PEM"})) "PKCS12", "PEM" })) String certType,
String certType,
@RequestParam(value = "key", required = false) @RequestParam(value = "key", required = false) @Parameter(description = "The private key for the digital certificate (required for PEM type certificates)") MultipartFile privateKeyFile,
@Parameter(description = "The private key for the digital certificate (required for PEM type certificates)")
MultipartFile privateKeyFile,
@RequestParam(value = "cert", required = false) @RequestParam(value = "cert", required = false) @Parameter(description = "The digital certificate (required for PEM type certificates)") MultipartFile certFile,
@Parameter(description = "The digital certificate (required for PEM type certificates)")
MultipartFile certFile,
@RequestParam(value = "p12", required = false) @RequestParam(value = "p12", required = false) @Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") MultipartFile p12File,
@Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)")
MultipartFile p12File,
@RequestParam(value = "password", required = false) @RequestParam(value = "password", required = false) @Parameter(description = "The password for the keystore or the private key") String password,
@Parameter(description = "The password for the keystore or the private key")
String password,
@RequestParam(value = "showSignature", required = false) @RequestParam(value = "showSignature", required = false) @Parameter(description = "Whether to visually show the signature in the PDF file") Boolean showSignature,
@Parameter(description = "Whether to visually show the signature in the PDF file")
Boolean showSignature,
@RequestParam(value = "reason", required = false) @RequestParam(value = "reason", required = false) @Parameter(description = "The reason for signing the PDF") String reason,
@Parameter(description = "The reason for signing the PDF")
String reason,
@RequestParam(value = "location", required = false) @RequestParam(value = "location", required = false) @Parameter(description = "The location where the PDF is signed") String location,
@Parameter(description = "The location where the PDF is signed")
String location,
@RequestParam(value = "name", required = false) @RequestParam(value = "name", required = false) @Parameter(description = "The name of the signer") String name,
@Parameter(description = "The name of the signer")
String name,
@RequestParam(value = "pageNumber", required = false) @RequestParam(value = "pageNumber", required = false) @Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") Integer pageNumber)
@Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") throws Exception {
Integer pageNumber) throws Exception {
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
PrivateKey privateKey = null; PrivateKey privateKey = null;
X509Certificate cert = null; X509Certificate cert = null;
if (certType != null) {
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();
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", provider);
if (isPEM(privateKeyFile.getBytes())) {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
} else {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
}
// Load certificate if (certType != null) {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider); logger.info("Cert type provided: {}", certType);
if (isPEM(certFile.getBytes())) { switch (certType) {
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); case "PKCS12":
} else { if (p12File != null) {
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certFile.getBytes())); KeyStore ks = KeyStore.getInstance("PKCS12");
} ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray());
} String alias = ks.aliases().nextElement();
break; 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()));
}
Principal principal = cert.getSubjectDN(); // Load certificate
String dn = principal.getName(); 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);
// Extract the "CN" (Common Name) field from the distinguished name (if it's present) // Load the PDF
String cn = null; try (PDDocument document = PDDocument.load(pdf.getBytes())) {
for (String part : dn.split(",")) { logger.info("Successfully loaded the provided PDF");
if (part.trim().startsWith("CN=")) { SignatureOptions signatureOptions = new SignatureOptions();
cn = part.trim().substring("CN=".length());
break;
}
}
// Set up the PDF reader and stamper
PdfReader reader = new PdfReader(new ByteArrayInputStream(pdf.getBytes()));
ByteArrayOutputStream signedPdf = new ByteArrayOutputStream();
PdfSigner signer = new PdfSigner(reader, signedPdf, new StampingProperties());
// Set up the signing appearance // If you want to show the signature
PdfSignatureAppearance appearance = signer.getSignatureAppearance()
.setReason("Test")
.setLocation("TestLocation");
if (showSignature != null && showSignature) { // ATTEMPT 2
float fontSize = 4; // the font size of the signature if (showSignature != null && showSignature) {
float marginRight = 36; // Margin from the right PDPage page = document.getPage(pageNumber - 1);
float marginBottom = 36; // Margin from the bottom
String signingDate = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date());
// Prepare the text for the digital signature PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
StringBuilder layer2TextBuilder = new StringBuilder(String.format("Digitally signed by: %s\nDate: %s", if (acroForm == null) {
name != null ? name : "Unknown", signingDate)); acroForm = new PDAcroForm(document);
document.getDocumentCatalog().setAcroForm(acroForm);
}
if (reason != null && !reason.isEmpty()) { // Create a new signature field and widget
layer2TextBuilder.append("\nReason: ").append(reason);
}
if (location != null && !location.isEmpty()) { PDSignatureField signatureField = new PDSignatureField(acroForm);
layer2TextBuilder.append("\nLocation: ").append(location); PDAnnotationWidget widget = signatureField.getWidgets().get(0);
} PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
String layer2Text = layer2TextBuilder.toString(); widget.setRectangle(rect);
// Get the PDF font and measure the width and height of the text block page.getAnnotations().add(widget);
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);
float textWidth = Arrays.stream(layer2Text.split("\n"))
.map(line -> font.getWidth(line, fontSize))
.max(Float::compare)
.orElse(0f);
int numLines = layer2Text.split("\n").length;
float textHeight = numLines * fontSize;
// Calculate the signature rectangle size // Set the appearance for the signature field
float sigWidth = textWidth + marginRight * 2; PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
float sigHeight = textHeight + marginBottom * 2; PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
appearanceStream.setResources(new PDResources());
appearanceStream.setBBox(rect);
appearanceDict.setNormalAppearance(appearanceStream);
widget.setAppearance(appearanceDict);
// Get the page size try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) {
PdfPage page = signer.getDocument().getPage(1); contentStream.beginText();
Rectangle pageSize = page.getPageSize(); 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();
}
// Define the position and dimension of the signature field // Add the widget annotation to the page
Rectangle rect = new Rectangle( page.getAnnotations().add(widget);
pageSize.getRight() - sigWidth - marginRight,
pageSize.getBottom() + marginBottom,
sigWidth,
sigHeight
);
// Configure the appearance of the digital signature // Add the signature field to the acroform
appearance.setPageRect(rect) acroForm.getFields().add(signatureField);
.setContact(name != null ? name : "")
.setPageNumber(pageNumber)
.setReason(reason != null ? reason : "")
.setLocation(location != null ? location : "")
.setReuseAppearance(false)
.setLayer2Text(layer2Text.toString());
signer.setFieldName("sig"); // Handle multiple signatures by ensuring a unique field name
} else { String baseFieldName = "Signature";
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION); String signatureFieldName = baseFieldName;
} int suffix = 1;
while (acroForm.getField(signatureFieldName) != null) {
// Set up the signer suffix++;
PrivateKeySignature pks = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName()); signatureFieldName = baseFieldName + suffix;
IExternalSignature pss = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName()); }
IExternalDigest digest = new BouncyCastleDigest(); signatureField.setPartialName(signatureFieldName);
}
document.addSignature(signature, signatureOptions);
logger.info("Signature added to the PDF document");
// External signing
ExternalSigningSupport externalSigning = document
.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
// Call iTex7 to sign the PDF byte[] content = IOUtils.toByteArray(externalSigning.getContent());
signer.signDetached(digest, pks, new Certificate[] {cert}, null, null, null, 0, PdfSigner.CryptoStandard.CMS);
// Using BouncyCastle to sign
System.out.println("Signed PDF size: " + signedPdf.size()); CMSTypedData cmsData = new CMSProcessableByteArray(content);
System.out.println("PDF signed = " + isPdfSigned(signedPdf.toByteArray())); CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
return WebResponseUtils.bytesToWebResponse(signedPdf.toByteArray(), "example.pdf"); ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
} .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey);
public boolean isPdfSigned(byte[] pdfData) throws IOException { gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
InputStream pdfStream = new ByteArrayInputStream(pdfData); new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build())
PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfStream)); .build(signer, cert));
SignatureUtil signatureUtil = new SignatureUtil(pdfDoc);
List<String> names = signatureUtil.getSignatureNames();
boolean isSigned = false; gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
CMSSignedData signedData = gen.generate(cmsData, false);
for (String name : names) { byte[] cmsSignature = signedData.getEncoded();
PdfPKCS7 pkcs7 = signatureUtil.readSignatureData(name); logger.info("About to sign content using BouncyCastle");
if (pkcs7 != null) { externalSigning.setSignature(cmsSignature);
System.out.println("Signature found."); logger.info("Signature set successfully");
// Log certificate details // After setting the signature, return the resultant PDF
Certificate[] signChain = pkcs7.getSignCertificateChain(); try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
for (Certificate cert : signChain) { document.save(signedPdfOutput);
if (cert instanceof X509Certificate) { return WebResponseUtils.boasToWebResponse(signedPdfOutput,
X509Certificate x509 = (X509Certificate) cert; pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
System.out.println("Certificate Details:");
System.out.println("Subject: " + x509.getSubjectDN());
System.out.println("Issuer: " + x509.getIssuerDN());
System.out.println("Serial: " + x509.getSerialNumber());
System.out.println("Not Before: " + x509.getNotBefore());
System.out.println("Not After: " + x509.getNotAfter());
}
}
isSigned = true; } catch (Exception e) {
} e.printStackTrace();
} }
} catch (Exception e) {
e.printStackTrace();
}
pdfDoc.close(); return null;
}
return isSigned; private byte[] parsePEM(byte[] content) throws IOException {
} PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
private byte[] parsePEM(byte[] content) throws IOException { return pemReader.readPemObject().getContent();
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");
}
} }

View file

@ -1,258 +0,0 @@
package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
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;
import org.bouncycastle.cms.CMSTypedData;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ContentDisposition;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.http.ResponseEntity;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
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.digitalsignature.visible.PDVisibleSignDesigner;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@RestController
@Tag(name = "Security", description = "Security APIs")
public class CertSignController2 {
private static final Logger logger = LoggerFactory.getLogger(CertSignController2.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[]> signPDF(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be signed")
MultipartFile pdf,
@RequestParam(value = "certType", required = false)
@Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {"PKCS12", "PEM"}))
String certType,
@RequestParam(value = "key", required = false)
@Parameter(description = "The private key for the digital certificate (required for PEM type certificates)")
MultipartFile privateKeyFile,
@RequestParam(value = "cert", required = false)
@Parameter(description = "The digital certificate (required for PEM type certificates)")
MultipartFile certFile,
@RequestParam(value = "p12", required = false)
@Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)")
MultipartFile p12File,
@RequestParam(value = "password", required = false)
@Parameter(description = "The password for the keystore or the private key")
String password,
@RequestParam(value = "showSignature", required = false)
@Parameter(description = "Whether to visually show the signature in the PDF file")
Boolean showSignature,
@RequestParam(value = "reason", required = false)
@Parameter(description = "The reason for signing the PDF")
String reason,
@RequestParam(value = "location", required = false)
@Parameter(description = "The location where the PDF is signed")
String location,
@RequestParam(value = "name", required = false)
@Parameter(description = "The name of the signer")
String name,
@RequestParam(value = "pageNumber", required = false)
@Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true")
Integer pageNumber) throws Exception {
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
PrivateKey privateKey = null;
X509Certificate cert = null;
if (certType != null) {
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();
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", provider);
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", provider);
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_DETACHED);
signature.setName(name);
signature.setLocation(location);
signature.setReason(reason);
// Load the PDF
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
SignatureOptions signatureOptions = new SignatureOptions();
// If you want to show the signature
if (showSignature != null && showSignature) {
// Calculate signature field position based on your requirements
PDPage page = document.getPage(pageNumber - 1); // zero-based
PDVisibleSignDesigner signDesigner = new PDVisibleSignDesigner(new ByteArrayInputStream(pdf.getBytes()));
//TODO signDesigner
PDVisibleSigProperties sigProperties = new PDVisibleSigProperties();
//TODO sigProperties extra
signatureOptions.setVisualSignature(sigProperties);
signatureOptions.setPage(pageNumber - 1);
}
document.addSignature(signature, signatureOptions);
// 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(provider).build(privateKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider(provider).build()).build(signer, cert));
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
CMSSignedData signedData = gen.generate(cmsData, false);
byte[] cmsSignature = signedData.getEncoded();
externalSigning.setSignature(cmsSignature);
// After setting the signature, return the resultant PDF
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
document.save(signedPdfOutput);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDisposition(ContentDisposition.builder("attachment").filename("signed.pdf").build());
return new ResponseEntity<>(signedPdfOutput.toByteArray(), headers, HttpStatus.OK);
}
}
}
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");
}
}

View file

@ -12,8 +12,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
public class WebResponseUtils { public class WebResponseUtils {
@ -61,18 +59,6 @@ public class WebResponseUtils {
return boasToWebResponse(baos, docName); return boasToWebResponse(baos, docName);
} }
public static ResponseEntity<byte[]> pdfDocToWebResponse(PdfDocument document, String docName) throws IOException {
// Open Byte Array and save document to it
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument newDocument = new PdfDocument(writer);
document.copyPagesTo(1, document.getNumberOfPages(), newDocument);
newDocument.close();
return boasToWebResponse(baos, docName);
}
} }