From ff9c0e9bd4a5e5fff4349c3c45ac36f92255a0b1 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Tue, 16 Sep 2025 15:27:30 +0100 Subject: [PATCH] lots of improvements --- .../ServerCertificateServiceInterface.java | 2 +- .../api/BookletImpositionController.java | 94 ++++++++++++------- .../controller/api/misc/ConfigController.java | 7 +- .../api/security/CertSignController.java | 8 +- .../api/general/BookletImpositionRequest.java | 2 +- .../ServerCertificateInitializer.java | 4 +- .../api/ServerCertificateController.java | 3 +- .../service/ServerCertificateService.java | 39 +++++++- 8 files changed, 107 insertions(+), 52 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/service/ServerCertificateServiceInterface.java b/app/common/src/main/java/stirling/software/common/service/ServerCertificateServiceInterface.java index df5dd0534..3d7c5d90b 100644 --- a/app/common/src/main/java/stirling/software/common/service/ServerCertificateServiceInterface.java +++ b/app/common/src/main/java/stirling/software/common/service/ServerCertificateServiceInterface.java @@ -39,4 +39,4 @@ public interface ServerCertificateServiceInterface { private final Date validFrom; private final Date validTo; } -} \ No newline at end of file +} diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java index 1ecfe2a6d..72010243b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java @@ -43,8 +43,8 @@ public class BookletImpositionController { summary = "Create a booklet with proper page imposition", description = "This operation combines page reordering for booklet printing with multi-page layout. " - + "It rearranges pages in the correct order for booklet printing and places multiple pages " - + "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO") + + "It rearranges pages in the correct order for booklet printing and places multiple pages " + + "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO") public ResponseEntity createBookletImposition( @ModelAttribute BookletImpositionRequest request) throws IOException { @@ -56,7 +56,8 @@ public class BookletImpositionController { // Validate pages per sheet for booklet if (pagesPerSheet != 2 && pagesPerSheet != 4) { - throw new IllegalArgumentException("pagesPerSheet must be 2 or 4 for booklet imposition"); + throw new IllegalArgumentException( + "pagesPerSheet must be 2 or 4 for booklet imposition"); } PDDocument sourceDocument = pdfDocumentFactory.load(file); @@ -65,9 +66,12 @@ public class BookletImpositionController { // Step 1: Reorder pages for booklet (reusing logic from RearrangePagesPDFController) List bookletOrder = getBookletPageOrder(bookletType, totalPages); - // Step 2: Create new document with multi-page layout (reusing logic from MultiPageLayoutController) - PDDocument newDocument = createBookletWithLayout(sourceDocument, bookletOrder, pagesPerSheet, addBorder, pageOrientation); - + // Step 2: Create new document with multi-page layout (reusing logic from + // MultiPageLayoutController) + PDDocument newDocument = + createBookletWithLayout( + sourceDocument, bookletOrder, pagesPerSheet, addBorder, pageOrientation); + sourceDocument.close(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -112,75 +116,93 @@ public class BookletImpositionController { } // Reused and adapted logic from MultiPageLayoutController - private PDDocument createBookletWithLayout(PDDocument sourceDocument, List pageOrder, - int pagesPerSheet, boolean addBorder, String pageOrientation) throws IOException { - - PDDocument newDocument = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); - + private PDDocument createBookletWithLayout( + PDDocument sourceDocument, + List pageOrder, + int pagesPerSheet, + boolean addBorder, + String pageOrientation) + throws IOException { + + PDDocument newDocument = + pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); + int cols = pagesPerSheet == 2 ? 2 : 2; // 2x1 for 2 pages, 2x2 for 4 pages int rows = pagesPerSheet == 2 ? 1 : 2; - + int currentPageIndex = 0; int totalOrderedPages = pageOrder.size(); - + while (currentPageIndex < totalOrderedPages) { // Use landscape orientation for booklets (A4 landscape -> A5 portrait when folded) - PDRectangle pageSize = "LANDSCAPE".equals(pageOrientation) ? - new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()) : PDRectangle.A4; + PDRectangle pageSize = + "LANDSCAPE".equals(pageOrientation) + ? new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()) + : PDRectangle.A4; PDPage newPage = new PDPage(pageSize); newDocument.addPage(newPage); - + float cellWidth = newPage.getMediaBox().getWidth() / cols; float cellHeight = newPage.getMediaBox().getHeight() / rows; - - PDPageContentStream contentStream = new PDPageContentStream( - newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); + + PDPageContentStream contentStream = + new PDPageContentStream( + newDocument, + newPage, + PDPageContentStream.AppendMode.APPEND, + true, + true); LayerUtility layerUtility = new LayerUtility(newDocument); - + if (addBorder) { contentStream.setLineWidth(1.5f); contentStream.setStrokingColor(Color.BLACK); } - + // Place pages on the current sheet - for (int sheetPosition = 0; sheetPosition < pagesPerSheet && currentPageIndex < totalOrderedPages; sheetPosition++) { + for (int sheetPosition = 0; + sheetPosition < pagesPerSheet && currentPageIndex < totalOrderedPages; + sheetPosition++) { int sourcePageIndex = pageOrder.get(currentPageIndex); PDPage sourcePage = sourceDocument.getPage(sourcePageIndex); PDRectangle rect = sourcePage.getMediaBox(); - + float scaleWidth = cellWidth / rect.getWidth(); float scaleHeight = cellHeight / rect.getHeight(); float scale = Math.min(scaleWidth, scaleHeight); - + int rowIndex = sheetPosition / cols; int colIndex = sheetPosition % cols; - + float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; - float y = newPage.getMediaBox().getHeight() - - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2); - + float y = + newPage.getMediaBox().getHeight() + - ((rowIndex + 1) * cellHeight + - (cellHeight - rect.getHeight() * scale) / 2); + contentStream.saveGraphicsState(); contentStream.transform(Matrix.getTranslateInstance(x, y)); contentStream.transform(Matrix.getScaleInstance(scale, scale)); - - PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, sourcePageIndex); + + PDFormXObject formXObject = + layerUtility.importPageAsForm(sourceDocument, sourcePageIndex); contentStream.drawForm(formXObject); - + contentStream.restoreGraphicsState(); - + if (addBorder) { float borderX = colIndex * cellWidth; float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight; contentStream.addRect(borderX, borderY, cellWidth, cellHeight); contentStream.stroke(); } - + currentPageIndex++; } - + contentStream.close(); } - + return newDocument; } -} \ No newline at end of file +} diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java index 150cbfaca..91dc4d161 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java @@ -13,8 +13,6 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; - import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.common.configuration.AppConfig; import stirling.software.common.model.ApplicationProperties; @@ -36,7 +34,7 @@ public class ConfigController { ApplicationContext applicationContext, EndpointConfiguration endpointConfiguration, @org.springframework.beans.factory.annotation.Autowired(required = false) - ServerCertificateServiceInterface serverCertificateService) { + ServerCertificateServiceInterface serverCertificateService) { this.applicationProperties = applicationProperties; this.applicationContext = applicationContext; this.endpointConfiguration = endpointConfiguration; @@ -76,7 +74,8 @@ public class ConfigController { configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled()); // Server certificate settings - configData.put("serverCertificateEnabled", + configData.put( + "serverCertificateEnabled", serverCertificateService != null && serverCertificateService.isEnabled()); // Legal settings diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 8ab23be25..ad68d4dbe 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -53,6 +53,7 @@ import org.bouncycastle.operator.InputDecryptorProvider; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; import org.bouncycastle.pkcs.PKCSException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -70,12 +71,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; - import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; -import stirling.software.common.service.ServerCertificateServiceInterface; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.ServerCertificateServiceInterface; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.WebResponseUtils; @@ -106,7 +105,8 @@ public class CertSignController { public CertSignController( CustomPDFDocumentFactory pdfDocumentFactory, - @Autowired(required = false) ServerCertificateServiceInterface serverCertificateService) { + @Autowired(required = false) + ServerCertificateServiceInterface serverCertificateService) { this.pdfDocumentFactory = pdfDocumentFactory; this.serverCertificateService = serverCertificateService; } diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/general/BookletImpositionRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/general/BookletImpositionRequest.java index 9d0612652..b7a7a5a1b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/general/BookletImpositionRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/general/BookletImpositionRequest.java @@ -37,4 +37,4 @@ public class BookletImpositionRequest extends PDFFile { requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"LANDSCAPE", "PORTRAIT"}) private String pageOrientation = "LANDSCAPE"; -} \ No newline at end of file +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/configuration/ServerCertificateInitializer.java b/app/proprietary/src/main/java/stirling/software/proprietary/configuration/ServerCertificateInitializer.java index d980a036e..6e82d1d99 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/configuration/ServerCertificateInitializer.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/configuration/ServerCertificateInitializer.java @@ -7,14 +7,14 @@ import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import stirling.software.common.service.ServerCertificateService; +import stirling.software.common.service.ServerCertificateServiceInterface; @Component @RequiredArgsConstructor @Slf4j public class ServerCertificateInitializer { - private final ServerCertificateService serverCertificateService; + private final ServerCertificateServiceInterface serverCertificateService; @EventListener(ApplicationReadyEvent.class) public void initializeServerCertificate() { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/ServerCertificateController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/ServerCertificateController.java index 4aae955e2..52d77e40c 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/ServerCertificateController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/ServerCertificateController.java @@ -112,8 +112,7 @@ public class ServerCertificateController { @GetMapping("/certificate") @Operation( summary = "Download server certificate", - description = - "Download the server certificate in DER format for validation purposes") + description = "Download the server certificate in DER format for validation purposes") public ResponseEntity getServerCertificate() { try { if (!serverCertificateService.hasServerCertificate()) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java index 900ab7773..a743b21fe 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java @@ -11,8 +11,14 @@ import java.security.cert.X509Certificate; import java.util.Date; import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; @@ -186,6 +192,36 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa new JcaX509v3CertificateBuilder( subject, serialNumber, notBefore, notAfter, subject, keyPair.getPublic()); + // Add PDF-specific certificate extensions for optimal PDF signing compatibility + JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + + // 1) End-entity certificate, not a CA (critical) + certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); + + // 2) Key usage for PDF digital signatures (critical) + certBuilder.addExtension( + Extension.keyUsage, + true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)); + + // 3) Extended key usage for document signing (non-critical, widely accepted) + certBuilder.addExtension( + Extension.extendedKeyUsage, + false, + new ExtendedKeyUsage(KeyPurposeId.id_kp_codeSigning)); + + // 4) Subject Key Identifier for chain building (non-critical) + certBuilder.addExtension( + Extension.subjectKeyIdentifier, + false, + extUtils.createSubjectKeyIdentifier(keyPair.getPublic())); + + // 5) Authority Key Identifier for self-signed cert (non-critical) + certBuilder.addExtension( + Extension.authorityKeyIdentifier, + false, + extUtils.createAuthorityKeyIdentifier(keyPair.getPublic())); + // Sign certificate ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA") @@ -213,5 +249,4 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa keyStore.store(fos, DEFAULT_PASSWORD.toCharArray()); } } - -} \ No newline at end of file +}