diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index c37fcc9c6e..b36f8c9077 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -1,8 +1,11 @@ package stirling.software.SPDF.controller.api.security; +import java.awt.Color; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.security.KeyStore; @@ -14,12 +17,38 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.Calendar; +import java.util.List; 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.common.PDStream; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName; +import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; +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.PDSignature; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDField; +import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; +import org.apache.pdfbox.util.Matrix; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMDecryptorProvider; import org.bouncycastle.openssl.PEMEncryptedKeyPair; @@ -35,6 +64,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; @@ -62,13 +92,103 @@ public class CertSignController { } class CreateSignature extends CreateSignatureBase { + File imageFile; + public CreateSignature(KeyStore keystore, char[] pin) throws KeyStoreException, - UnrecoverableKeyException, - NoSuchAlgorithmException, - IOException, - CertificateException { + UnrecoverableKeyException, + NoSuchAlgorithmException, + IOException, + CertificateException { super(keystore, pin); + ClassPathResource resource = new ClassPathResource("static/images/signature.png"); + imageFile = resource.getFile(); + } + + public InputStream createVisibleSignature(PDDocument srcDoc, PDSignature signature, Integer pageNumber, + Boolean showImage) throws IOException { + // modified from org.apache.pdfbox.examples.signature.CreateVisibleSignature2 + try (PDDocument doc = new PDDocument()) { + PDPage page = new PDPage(srcDoc.getPage(pageNumber).getMediaBox()); + doc.addPage(page); + PDAcroForm acroForm = new PDAcroForm(doc); + doc.getDocumentCatalog().setAcroForm(acroForm); + PDSignatureField signatureField = new PDSignatureField(acroForm); + PDAnnotationWidget widget = signatureField.getWidgets().get(0); + List acroFormFields = acroForm.getFields(); + acroForm.setSignaturesExist(true); + acroForm.setAppendOnly(true); + acroForm.getCOSObject().setDirect(true); + acroFormFields.add(signatureField); + + PDRectangle rect = new PDRectangle(0, 0, 200, 50); + + widget.setRectangle(rect); + + // from PDVisualSigBuilder.createHolderForm() + PDStream stream = new PDStream(doc); + PDFormXObject form = new PDFormXObject(stream); + PDResources res = new PDResources(); + form.setResources(res); + form.setFormType(1); + PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight()); + float height = bbox.getHeight(); + form.setBBox(bbox); + PDFont font = new PDType1Font(FontName.TIMES_BOLD); + + // from PDVisualSigBuilder.createAppearanceDictionary() + PDAppearanceDictionary appearance = new PDAppearanceDictionary(); + appearance.getCOSObject().setDirect(true); + PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject()); + appearance.setNormalAppearance(appearanceStream); + widget.setAppearance(appearance); + + try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) { + if (showImage) { + cs.saveGraphicsState(); + PDExtendedGraphicsState extState = new PDExtendedGraphicsState(); + extState.setBlendMode(BlendMode.MULTIPLY); + extState.setNonStrokingAlphaConstant(0.5f); + cs.setGraphicsStateParameters(extState); + cs.transform(Matrix.getScaleInstance(0.08f, 0.08f)); + PDImageXObject img = PDImageXObject.createFromFileByExtension(imageFile, + doc); + cs.drawImage(img, 100, 0); + cs.restoreGraphicsState(); + } + + // show text + float fontSize = 10; + float leading = fontSize * 1.5f; + cs.beginText(); + cs.setFont(font, fontSize); + cs.setNonStrokingColor(Color.black); + cs.newLineAtOffset(fontSize, height - leading); + cs.setLeading(leading); + + X509Certificate cert = (X509Certificate) getCertificateChain()[0]; + + // https://stackoverflow.com/questions/2914521/ + X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); + RDN cn = x500Name.getRDNs(BCStyle.CN)[0]; + String name = IETFUtils.valueToString(cn.getFirst().getValue()); + + String date = signature.getSignDate().getTime().toString(); + String reason = signature.getReason(); + + cs.showText("Signed by " + name); + cs.newLine(); + cs.showText(date); + cs.newLine(); + cs.showText(reason); + + cs.endText(); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + doc.save(baos); + return new ByteArrayInputStream(baos.toByteArray()); + } } } @@ -80,10 +200,7 @@ public CertSignController(CustomPDDocumentFactory pdfDocumentFactory) { } @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:SISO") + @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:SISO") public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception { MultipartFile pdf = request.getFileInput(); @@ -97,7 +214,7 @@ public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertReq String reason = request.getReason(); String location = request.getLocation(); String name = request.getName(); - Integer pageNumber = request.getPageNumber(); + Integer pageNumber = request.getPageNumber() - 1; if (certType == null) { throw new IllegalArgumentException("Cert type must be provided"); @@ -112,7 +229,7 @@ public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertReq PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password); Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes()); ks.setKeyEntry( - "alias", privateKey, password.toCharArray(), new Certificate[] {cert}); + "alias", privateKey, password.toCharArray(), new Certificate[] { cert }); break; case "PKCS12": ks = KeyStore.getInstance("PKCS12"); @@ -126,11 +243,10 @@ public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertReq throw new IllegalArgumentException("Invalid cert type: " + certType); } - // TODO: page number - CreateSignature createSignature = new CreateSignature(ks, password.toCharArray()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - sign(pdfDocumentFactory, pdf.getBytes(), baos, createSignature, name, location, reason); + sign(pdfDocumentFactory, pdf.getBytes(), baos, createSignature, showSignature, pageNumber, name, location, + reason); return WebResponseUtils.boasToWebResponse( baos, Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "") @@ -142,6 +258,8 @@ private static void sign( byte[] input, OutputStream output, CreateSignature instance, + Boolean showSignature, + Integer pageNumber, String name, String location, String reason) { @@ -154,7 +272,17 @@ private static void sign( signature.setReason(reason); signature.setSignDate(Calendar.getInstance()); - doc.addSignature(signature, instance); + if (showSignature) { + SignatureOptions signatureOptions = new SignatureOptions(); + signatureOptions + .setVisualSignature(instance.createVisibleSignature(doc, signature, pageNumber, true)); + signatureOptions.setPage(pageNumber); + + doc.addSignature(signature, instance, signatureOptions); + + } else { + doc.addSignature(signature, instance); + } doc.saveIncremental(output); } catch (Exception e) { logger.error("exception", e); @@ -163,22 +291,19 @@ private static void sign( private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password) throws IOException, OperatorCreationException, PKCSException { - try (PEMParser pemParser = - new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) { + try (PEMParser pemParser = new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) { Object pemObject = pemParser.readObject(); JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); PrivateKeyInfo pkInfo; if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) { - InputDecryptorProvider decProv = - new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray()); + InputDecryptorProvider decProv = new JceOpenSSLPKCS8DecryptorProviderBuilder() + .build(password.toCharArray()); pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv); } else if (pemObject instanceof PEMEncryptedKeyPair) { - PEMDecryptorProvider decProv = - new JcePEMDecryptorProviderBuilder().build(password.toCharArray()); - pkInfo = - ((PEMEncryptedKeyPair) pemObject) - .decryptKeyPair(decProv) - .getPrivateKeyInfo(); + PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password.toCharArray()); + pkInfo = ((PEMEncryptedKeyPair) pemObject) + .decryptKeyPair(decProv) + .getPrivateKeyInfo(); } else { pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo(); } diff --git a/src/main/resources/static/images/signature.png b/src/main/resources/static/images/signature.png new file mode 100644 index 0000000000..1adfcedc3c Binary files /dev/null and b/src/main/resources/static/images/signature.png differ diff --git a/src/main/resources/templates/security/cert-sign.html b/src/main/resources/templates/security/cert-sign.html index 173cd06d0a..4c3a34d0e3 100644 --- a/src/main/resources/templates/security/cert-sign.html +++ b/src/main/resources/templates/security/cert-sign.html @@ -71,7 +71,7 @@
- +