diff --git a/.github/labeler.yml b/.github/labeler.yml index d4d332657..20650649f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,8 +4,8 @@ functions: onboarding-ms: - apps/onboarding-ms/** -onboarding-sdk: -- apps/onboarding-sdk/** +libs: +- libs ops: - .github/** diff --git a/libs/onboarding-sdk-crypto/.gitignore b/libs/onboarding-sdk-crypto/.gitignore new file mode 100644 index 000000000..5ff6309b7 --- /dev/null +++ b/libs/onboarding-sdk-crypto/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/libs/onboarding-sdk-crypto/README.md b/libs/onboarding-sdk-crypto/README.md new file mode 100644 index 000000000..44329c4f6 --- /dev/null +++ b/libs/onboarding-sdk-crypto/README.md @@ -0,0 +1,60 @@ +# Onboarding SDK Crypto + +This module contains utilities to perform cryptographic operation, such digital signatures. + +See [Confluence page](https://pagopa.atlassian.net/wiki/spaces/SCP/pages/616857618/Firma+digitale+per+mezzo+dei+servizi+di+Aruba) +for integration and documentation details + + +### Hash signature sources + +It is possible to configure different hash signature sources. + +The sources available inside this repository are: + +* Pkcs7HashSignService + +## Pkcs7HashSignService + +It will use the provided private key and certificate, you must set these env variables. + +| Properties | Description | Default | +|--------------------|---------------------------------------------------------------------------------|---------| +| crypto.key.cert | The private key (PEM) used when the pkcs7 hash signature source is local | | +| crypto.key.private | The certificate (PEM) used when the pkcs7 hash signature source is local | | + + +## Installation + +To use this library in your projects, you can add the dependency to your pom.xml if you're using Maven: + +```shell script + + it.pagopa.selfcare + onboarding-sdk-crypto + 0.1.0 + +``` +If you are using Gradle, you can add the dependency to your build.gradle file: + +```shell script +dependencies { + implementation 'it.pagopa.selfcare:onboarding-sdk-crypto:0.1.0' +} +``` + +## Usage + +You can inject the service in the context of Quarkus or Spring (replace @ApplicationScoped with @Bean). + +```java script + @ApplicationScoped + public Pkcs7HashSignService pkcs7HashSignService(){ + return new Pkcs7HashSignServiceImpl(); + } + + @ApplicationScoped + public PadesSignService padesSignService(Pkcs7HashSignService pkcs7HashSignService){ + return new PadesSignServiceImpl(pkcs7HashSignService); + } + ``` \ No newline at end of file diff --git a/libs/onboarding-sdk-crypto/pom.xml b/libs/onboarding-sdk-crypto/pom.xml new file mode 100644 index 000000000..5a4553efe --- /dev/null +++ b/libs/onboarding-sdk-crypto/pom.xml @@ -0,0 +1,72 @@ + + 4.0.0 + + it.pagopa.selfcare + onboarding-sdk-pom + 0.1.0 + ../onboarding-sdk-pom + + onboarding-sdk-crypto + onboarding-sdk-crypto + http://maven.apache.org + + + + + jakarta.xml.soap + jakarta.xml.soap-api + 3.0.1 + + + jakarta.xml.ws + jakarta.xml.ws-api + 4.0.1 + + + com.sun.xml.ws + jaxws-rt + 4.0.2 + + + jakarta.activation + jakarta.activation-api + + + jakarta.xml.bind + jakarta.xml.bind-api + + + com.sun.xml.bind + jaxb-impl + 4.0.1 + + + com.sun.xml.messaging.saaj + saaj-impl + 3.0.0 + + + org.slf4j + slf4j-api + + + + org.apache.pdfbox + pdfbox + 2.0.27 + + + org.bouncycastle + bcprov-jdk18on + + + org.bouncycastle + bcpkix-jdk18on + + + org.apache.commons + commons-lang3 + + + diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/PadesSignService.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/PadesSignService.java new file mode 100644 index 000000000..5ebeb709c --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/PadesSignService.java @@ -0,0 +1,10 @@ +package it.pagopa.selfcare.onboarding.crypto; + + +import it.pagopa.selfcare.onboarding.crypto.entity.SignatureInformation; + +import java.io.File; + +public interface PadesSignService { + void padesSign(File pdfFile, File signedPdfFile, SignatureInformation signInfo); +} diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/PadesSignServiceImpl.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/PadesSignServiceImpl.java new file mode 100644 index 000000000..a08acc7fc --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/PadesSignServiceImpl.java @@ -0,0 +1,43 @@ +package it.pagopa.selfcare.onboarding.crypto; + +import it.pagopa.selfcare.onboarding.crypto.entity.SignatureInformation; +import it.pagopa.selfcare.onboarding.crypto.utils.CryptoUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Calendar; + +public class PadesSignServiceImpl implements PadesSignService { + private final Pkcs7HashSignService pkcs7Signature; + + public PadesSignServiceImpl(Pkcs7HashSignService pkcs7Signature) { + this.pkcs7Signature = pkcs7Signature; + } + + public void padesSign(File pdfFile, File signedPdfFile, SignatureInformation signInfo) { + CryptoUtils.createParentDirectoryIfNotExists(signedPdfFile); + + try (FileOutputStream fos = new FileOutputStream(signedPdfFile); + PDDocument doc = PDDocument.load(pdfFile)){ + + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); + signature.setName(signInfo.getName()); + signature.setLocation(signInfo.getLocation()); + signature.setReason(signInfo.getReason()); + signature.setSignDate(Calendar.getInstance()); + SignatureOptions signatureOptions = new SignatureOptions(); + signatureOptions.setPreferredSignatureSize(35944); + doc.addSignature(signature, this.pkcs7Signature, signatureOptions); + doc.saveIncremental(fos); + + } catch (Exception var12) { + throw new IllegalStateException(String.format("Something gone wrong while signing input pdf %s and storing it into %s", pdfFile.getAbsolutePath(), signedPdfFile.getAbsolutePath()), var12); + } + } +} + diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/Pkcs7HashSignService.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/Pkcs7HashSignService.java new file mode 100644 index 000000000..f15152ac6 --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/Pkcs7HashSignService.java @@ -0,0 +1,6 @@ +package it.pagopa.selfcare.onboarding.crypto; + +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; + +public interface Pkcs7HashSignService extends SignatureInterface { +} diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/Pkcs7HashSignServiceImpl.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/Pkcs7HashSignServiceImpl.java new file mode 100644 index 000000000..45f093e12 --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/Pkcs7HashSignServiceImpl.java @@ -0,0 +1,57 @@ +package it.pagopa.selfcare.onboarding.crypto; + + +import it.pagopa.selfcare.onboarding.crypto.config.LocalCryptoConfig; +import it.pagopa.selfcare.onboarding.crypto.config.LocalCryptoInitializer; +import it.pagopa.selfcare.onboarding.crypto.utils.CMSTypedDataInputStream; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.*; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +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.util.Store; + +import java.io.*; +import java.security.cert.CertificateEncodingException; +import java.util.Collections; + +/** + * Implementation of {@link Pkcs7HashSignService} which will use provided private and public keys to perform sign operations + */ +public class Pkcs7HashSignServiceImpl implements Pkcs7HashSignService { + + private final CMSSignedDataGenerator cmsSignGenerator; + + public Pkcs7HashSignServiceImpl() { + try { + LocalCryptoConfig localCryptoConfig = LocalCryptoInitializer.initializeConfig(); + BouncyCastleProvider bc = new BouncyCastleProvider(); + Store certStore = new JcaCertStore(Collections.singletonList(localCryptoConfig.getCertificate())); + + cmsSignGenerator = new CMSSignedDataGenerator(); + ContentSigner sha512Signer = new JcaContentSignerBuilder("SHA256WithRSA").setProvider(bc).build(localCryptoConfig.getPrivateKey()); + + cmsSignGenerator.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().setProvider(bc).build()).build(sha512Signer, new X509CertificateHolder(localCryptoConfig.getCertificate().getEncoded()) + )); + cmsSignGenerator.addCertificates(certStore); + } catch (CertificateEncodingException | OperatorCreationException | CMSException | IOException e) { + throw new IllegalStateException("Something gone wrong while initializing CertStore using provided private and public key", e); + } + } + + public byte[] sign(InputStream is) throws IOException { + try { + CMSTypedDataInputStream msg = new CMSTypedDataInputStream(is); + CMSSignedData signedData = cmsSignGenerator.generate(msg, false); + return signedData.getEncoded(); + } catch (CMSException e) { + throw new IllegalArgumentException("Something gone wrong while performing pkcs7 hash sign", e); + } + } + +} diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/config/LocalCryptoConfig.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/config/LocalCryptoConfig.java new file mode 100644 index 000000000..049e43e9a --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/config/LocalCryptoConfig.java @@ -0,0 +1,26 @@ +package it.pagopa.selfcare.onboarding.crypto.config; + +import java.security.PrivateKey; +import java.security.cert.Certificate; + +public class LocalCryptoConfig { + + private Certificate certificate; + private PrivateKey privateKey; + + public Certificate getCertificate() { + return certificate; + } + + public void setCertificate(Certificate certificate) { + this.certificate = certificate; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + } +} diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/config/LocalCryptoInitializer.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/config/LocalCryptoInitializer.java new file mode 100644 index 000000000..8c3e5ff13 --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/config/LocalCryptoInitializer.java @@ -0,0 +1,29 @@ +package it.pagopa.selfcare.onboarding.crypto.config; + +import it.pagopa.selfcare.onboarding.crypto.utils.CryptoUtils; +import org.apache.commons.lang3.StringUtils; + + +public class LocalCryptoInitializer { + + private LocalCryptoInitializer(){} + + public static LocalCryptoConfig initializeConfig() { + LocalCryptoConfig config = new LocalCryptoConfig(); + String cert = System.getProperty("crypto.key.cert"); + String pKey = System.getProperty("crypto.key.private"); + + try { + if(StringUtils.isBlank(cert) || StringUtils.isBlank(pKey)){ + throw new IllegalStateException("Define private and cert values in order to perform locally sign operations"); + } + + config.setCertificate(CryptoUtils.getCertificate(cert)); + config.setPrivateKey(CryptoUtils.getPrivateKey(pKey)); + } catch (Exception e) { + throw new IllegalStateException("Something gone wrong while loading crypto private and public keys", e); + } + + return config; + } +} diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/entity/SignatureInformation.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/entity/SignatureInformation.java new file mode 100644 index 000000000..d12a5f6ce --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/entity/SignatureInformation.java @@ -0,0 +1,37 @@ +package it.pagopa.selfcare.onboarding.crypto.entity; + +public class SignatureInformation { + private String name; + private String location; + private String reason; + + public SignatureInformation(String name, String location, String reason) { + this.name = name; + this.location = location; + this.reason = reason; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } +} diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/utils/CMSTypedDataInputStream.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/utils/CMSTypedDataInputStream.java new file mode 100644 index 000000000..01de4d265 --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/utils/CMSTypedDataInputStream.java @@ -0,0 +1,37 @@ +package it.pagopa.selfcare.onboarding.crypto.utils; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.cms.CMSTypedData; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class CMSTypedDataInputStream implements CMSTypedData { + InputStream in; + + public CMSTypedDataInputStream(InputStream is) { + in = is; + } + + @Override + public ASN1ObjectIdentifier getContentType() { + return PKCSObjectIdentifiers.data; + } + + @Override + public Object getContent() { + return in; + } + + @Override + public void write(OutputStream out) throws IOException { + byte[] buffer = new byte[8 * 1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + in.close(); + } +} \ No newline at end of file diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/utils/CryptoUtils.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/utils/CryptoUtils.java new file mode 100644 index 000000000..1984fdde7 --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/utils/CryptoUtils.java @@ -0,0 +1,76 @@ +package it.pagopa.selfcare.onboarding.crypto.utils; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +public class CryptoUtils { + + private CryptoUtils() {} + + public static void createParentDirectoryIfNotExists(File destFile) { + Path destDir = destFile.toPath().getParent(); + if (!Files.exists(destDir)) { + try { + Files.createDirectories(destDir); + } catch (IOException var3) { + throw new IllegalArgumentException(String.format("Something gone wrong while creating destination folder: %s", destDir), var3); + } + } + + } + + + + public static byte[] getDigest(InputStream is) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(is.readAllBytes()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Something gone wrong selecting digest algorithm", e); + } catch (IOException e) { + throw new IllegalArgumentException("Something gone wrong while reading inputStream", e); + } + } + + public static X509Certificate getCertificate(String cert) throws IOException, CertificateException { + try( + InputStream is = new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8)) + ) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(is); + } + } + + public static RSAPrivateKey getPrivateKey(String privateKey) throws InvalidKeySpecException, NoSuchAlgorithmException, IOException { + String keyStringFormat = pemToString(privateKey); + try( + InputStream is = new ByteArrayInputStream(Base64.getDecoder().decode(keyStringFormat)) + ) { + PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(is.readAllBytes()); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return (RSAPrivateKey) kf.generatePrivate(encodedKeySpec); + } + } + + public static String pemToString(String target) { + return target + .replaceAll("^-----BEGIN[A-Z|\\s]+-----", "") + .replaceAll("\\n+", "") + .replaceAll("-----END[A-Z|\\s]+-----$", ""); + } +} diff --git a/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/utils/SoapLoggingHandler.java b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/utils/SoapLoggingHandler.java new file mode 100644 index 000000000..30300b30f --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/main/java/it/pagopa/selfcare/onboarding/crypto/utils/SoapLoggingHandler.java @@ -0,0 +1,63 @@ +package it.pagopa.selfcare.onboarding.crypto.utils; + +import jakarta.xml.soap.SOAPException; +import jakarta.xml.soap.SOAPMessage; +import jakarta.xml.ws.handler.MessageContext; +import jakarta.xml.ws.handler.soap.SOAPHandler; +import jakarta.xml.ws.handler.soap.SOAPMessageContext; +import org.slf4j.Logger; + +import javax.xml.namespace.QName; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + + +public class SoapLoggingHandler implements SOAPHandler { + + private static final Logger log = org.slf4j.LoggerFactory.getLogger(SoapLoggingHandler.class); + + @Override + public void close(MessageContext msg) { + // Do Nothing + } + + @Override + public boolean handleFault(SOAPMessageContext msg) { + SOAPMessage message = msg.getMessage(); + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + message.writeTo(outputStream); + log.info("Obtained a fault message: {}", outputStream); + } catch (SOAPException | IOException e) { + log.error("Something gone wrong while tracing soap fault message"); + } + return true; + } + + @Override + public boolean handleMessage(SOAPMessageContext msg) { + if(log.isDebugEnabled()) { + SOAPMessage message = msg.getMessage(); + boolean isOutboundMessage = (Boolean) msg.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); + String msgType = isOutboundMessage + ? "OUTBOUND MESSAGE" + : "INBOUND MESSAGE"; + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + message.writeTo(outputStream); + log.debug("Obtained a {} message: {}", msgType, outputStream); + } catch (SOAPException | IOException e) { + log.error(String.format("Something gone wrong while tracing soap %s", msgType)); + } + } + return true; + } + + @Override + public Set getHeaders() { + return Collections.emptySet(); + } + +} \ No newline at end of file diff --git a/libs/onboarding-sdk-crypto/src/test/java/it/pagopa/selfcare/onboarding/crypto/PadesSignServiceTest.java b/libs/onboarding-sdk-crypto/src/test/java/it/pagopa/selfcare/onboarding/crypto/PadesSignServiceTest.java new file mode 100644 index 000000000..2dd64efe6 --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/test/java/it/pagopa/selfcare/onboarding/crypto/PadesSignServiceTest.java @@ -0,0 +1,140 @@ +package it.pagopa.selfcare.onboarding.crypto; + + +import it.pagopa.selfcare.onboarding.crypto.config.LocalCryptoConfig; +import it.pagopa.selfcare.onboarding.crypto.entity.SignatureInformation; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.io.IOUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageTree; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.util.Store; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class PadesSignServiceTest { + + private PadesSignService service; + + // mocked data will not be aligned with timestamp alway updated, thus base test could not successfully sign + protected boolean verifySignerInformation = true; + protected Path inputFilePath = Path.of("src/test/resources/signTest.pdf"); + + private Pkcs7HashSignService pkcs7HashSignService; + + @BeforeEach + void setup() throws IOException { + pkcs7HashSignService = mock(Pkcs7HashSignService.class); + service = new PadesSignServiceImpl(pkcs7HashSignService); + } + + @Test + protected void testPadesSign() throws IOException, GeneralSecurityException, OperatorCreationException, CMSException { + File inputFile = inputFilePath.toFile(); + File outputFile = getOutputPadesFile(); + if (outputFile.exists()) { + Assertions.assertTrue(outputFile.delete()); + } + + when(pkcs7HashSignService.sign(any())).thenReturn(Files.readAllBytes(inputFilePath)); + + service.padesSign(inputFile, outputFile, new SignatureInformation("PagoPA S.P.A", "Rome", "onboarding contract")); + Assertions.assertTrue(outputFile.exists()); + + checkPadesSignature(inputFile, outputFile); + } + + protected File getOutputPadesFile() { + return Path.of("target/tmp/signedSignTest-selfSigned.pdf").toFile(); + } + + @SuppressWarnings("unchecked") + private void checkPadesSignature(File origFile, File signedFile) + throws IOException, CMSException, OperatorCreationException, GeneralSecurityException + { + PDDocument document = PDDocument.load(origFile); + // get string representation of pages COSObject + String origPageKey = document.getDocumentCatalog().getCOSObject().getItem(COSName.PAGES).toString(); + document.close(); + + document = PDDocument.load(signedFile); + + // early detection of problems in the page structure + int p = 0; + PDPageTree pageTree = document.getPages(); + for (PDPage page : document.getPages()) + { + Assertions.assertEquals(p, pageTree.indexOf(page)); + ++p; + } + + Assertions.assertEquals(origPageKey, document.getDocumentCatalog().getCOSObject().getItem(COSName.PAGES).toString()); + + List signatureDictionaries = document.getSignatureDictionaries(); + if (signatureDictionaries.isEmpty()) + { + Assertions.fail("no signature found"); + } + for (PDSignature sig : document.getSignatureDictionaries()) + { + byte[] contents = sig.getContents(); + + byte[] buf = sig.getSignedContent(new FileInputStream(signedFile)); + + // verify that getSignedContent() brings the same content + // regardless whether from an InputStream or from a byte array + FileInputStream fis2 = new FileInputStream(signedFile); + byte[] buf2 = sig.getSignedContent(IOUtils.toByteArray(fis2)); + Assertions.assertArrayEquals(buf, buf2); + fis2.close(); + + // verify that all getContents() methods returns the same content + FileInputStream fis3 = new FileInputStream(signedFile); + byte[] contents2 = sig.getContents(IOUtils.toByteArray(fis3)); + Assertions.assertArrayEquals(contents, contents2); + fis3.close(); + byte[] contents3 = sig.getContents(new FileInputStream(signedFile)); + Assertions.assertArrayEquals(contents, contents3); + } + document.close(); + } + + @Test + void testHandleException() throws IOException { + File inputFile = inputFilePath.toFile(); + File outputFile = getOutputPadesFile(); + if (outputFile.exists()) { + Assertions.assertTrue(outputFile.delete()); + } + + + when(pkcs7HashSignService.sign(any())).thenThrow(new RuntimeException()); + assertThrows(IllegalStateException.class, () -> service.padesSign(inputFile, outputFile, new SignatureInformation("PagoPA S.P.A", "Rome", "onboarding contract"))); + } +} \ No newline at end of file diff --git a/libs/onboarding-sdk-crypto/src/test/java/it/pagopa/selfcare/onboarding/crypto/Pkcs7HashSignServiceTest.java b/libs/onboarding-sdk-crypto/src/test/java/it/pagopa/selfcare/onboarding/crypto/Pkcs7HashSignServiceTest.java new file mode 100644 index 000000000..6059cec79 --- /dev/null +++ b/libs/onboarding-sdk-crypto/src/test/java/it/pagopa/selfcare/onboarding/crypto/Pkcs7HashSignServiceTest.java @@ -0,0 +1,90 @@ +package it.pagopa.selfcare.onboarding.crypto; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Base64; + +class Pkcs7HashSignServiceTest { + + private static final String testCert = "-----BEGIN CERTIFICATE-----\n" + + "MIIDnzCCAoegAwIBAgIUJ8/0z+sR6Llr9FcIGoc5nvZQydgwDQYJKoZIhvcNAQEL\n" + + "BQAwXzELMAkGA1UEBhMCSVQxDTALBgNVBAgMBFJPTUUxDTALBgNVBAcMBFJPTUUx\n" + + "DjAMBgNVBAoMBUlEUEFZMQ4wDAYDVQQLDAVJRFBBWTESMBAGA1UEAwwJbG9jYWxo\n" + + "b3N0MB4XDTIyMTEwOTE1MTI0NFoXDTMyMDkxNzE1MTI0NFowXzELMAkGA1UEBhMC\n" + + "SVQxDTALBgNVBAgMBFJPTUUxDTALBgNVBAcMBFJPTUUxDjAMBgNVBAoMBUlEUEFZ\n" + + "MQ4wDAYDVQQLDAVJRFBBWTESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG\n" + + "9w0BAQEFAAOCAQ8AMIIBCgKCAQEArDOJKswwCaKdYJbaHZz3bgEIl7z1ArZpNI54\n" + + "ZGaXcRitiwjr/W9fenW69mG7IAlITuPtaIu4iggXTcSRuaulres2EvuP7KjL0tfo\n" + + "x/PstqaMZzLF8wOYfJE4iJ8ffcQL67LJ3/Wwn2FhYVV+4D2AYW8QPdRm406HJG7b\n" + + "NKLmdM9AFUQp6zoTvNegyWQyAfH40i72UopltDubcAykD6YgkRctCtKd8h/BRpIR\n" + + "tMn0AGLM/o5qwYu+eCAy8/7Ppj3HzCwHkDOJad/g2pRj4soJdvn5rP6TM4OVtZ7V\n" + + "ehxionkaccBPcyDGSrIo5837XYaGv3r7Rn0rCplfxnU4Gtmd5wIDAQABo1MwUTAd\n" + + "BgNVHQ4EFgQUPYfJeHRHwSLmcueB8jUQSHUReVIwHwYDVR0jBBgwFoAUPYfJeHRH\n" + + "wSLmcueB8jUQSHUReVIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC\n" + + "AQEAK34LEHSVM44Wwbs9nKDKeQTRGosdd+gQSrqGf3nI0vkhckuaoYPnuFKi+eo2\n" + + "r+J6xXgqhQfrvhXnYxNEJr9U+9ELBc3IjG6bTUS6HyWhu2PJCeckxQJqonVntl99\n" + + "jmEr4G7QJeDc9oJmC0NJqBmQS/D0tMxChNWpYe1AoGXwqc4S6NTd3x2Z8THzv8du\n" + + "MMn7+1f/VOWe7/Iuuvx5DHN2JFi0lvhMqwglIweGn/qLGB0+r9GM+QlfGuZvUey2\n" + + "x3C0DLQnNIkNKktGjaNjCmpZcd9SIVi6TOPpR+AxlIddYvUXu4GYVXyfDPgzPeha\n" + + "JDiI4WMkIMmYSzhMc/lfuDMGow==\n" + + "-----END CERTIFICATE-----"; + + private static final String testPrivateKey = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCsM4kqzDAJop1g\n" + + "ltodnPduAQiXvPUCtmk0jnhkZpdxGK2LCOv9b196dbr2YbsgCUhO4+1oi7iKCBdN\n" + + "xJG5q6Wt6zYS+4/sqMvS1+jH8+y2poxnMsXzA5h8kTiInx99xAvrssnf9bCfYWFh\n" + + "VX7gPYBhbxA91GbjTockbts0ouZ0z0AVRCnrOhO816DJZDIB8fjSLvZSimW0O5tw\n" + + "DKQPpiCRFy0K0p3yH8FGkhG0yfQAYsz+jmrBi754IDLz/s+mPcfMLAeQM4lp3+Da\n" + + "lGPiygl2+fms/pMzg5W1ntV6HGKieRpxwE9zIMZKsijnzftdhoa/evtGfSsKmV/G\n" + + "dTga2Z3nAgMBAAECggEAEC6FmMJ4Tyd7T3zNgVPjQnCRbKTihz858qjislibqZKO\n" + + "mE6d0oJ5P+o5R/bWHUQSCevMPvNGQ55QBkxO/1ocZxP/0FfYZf5UrPsCEmwfFejf\n" + + "r8DrLhNr7GS/IcOGM4zNK/hwlP2i+88sVfexRQQygLVtmsnPY1PZSjiqm68lJdu+\n" + + "aP8TYM10y1aeiYnfuUYvnvXJFXeTEockhaUJTmeIQNbbUy+pyJ0mAPASPtXRLr8h\n" + + "UflutICnWcx4v/qkCn1jmHw+NMA4q7hOH7UuOAqj53FqGMN+IWfjMmmYoQ7MVURx\n" + + "8CrnEtlCOua+C8EEIFL2ylvV7X0cv/DqCJLVQoegsQKBgQDLzMaAjNgD8xSXp+Gj\n" + + "beeUsSGptEaGMuA89AzyTnCyvU9a1HGwDAghoQPae+pVk7R5uokojWkBVzP/kKxv\n" + + "ZldGwPOegUUdBLS4yJML+OkqtoCgf3Mbcozm5dVYtx7bYdhh3PswzRmn/h/YjEAz\n" + + "+/mxi6dJir0k0Nd4YNtQbzBctwKBgQDYTtSmJvVQdOHnzqA/LRmMF1l+HaqLuDfu\n" + + "B4rDlxCdDfOAvHqz+3YapP3B4MQuz29TSDqwAnzoN2XZX5B6g/jKauWpAwZkFXuO\n" + + "fqcfNG/+MewTcHIYNm+EtgXtIsnCXDfAeXdQapzNsOX+XSF/aWcgGHg18xOBPt0R\n" + + "7Aoa/h34UQKBgQCsCzGjwcJ2CxXeNPYxfg1ao/HUDoDet0I/kpL/VqKi8Vd1SRS0\n" + + "VmPi58eWALfBCJD5ljRFjKMRY6lc3KgE3vNconTG4UAUEC30NDaWi8liqnCJjS4C\n" + + "BMDYBzwEyYn+D2qYqvFOsEYxYEFIEJX+jH+sl0VguwOTec38LF/YVhUQnwKBgG5u\n" + + "2Kw3SZkZA1ioqjF24gsexKbZmH+avps8qICw+F9mhwIbt/15jVOPFqrMCPzpFKoN\n" + + "P0ErFAAugEYZPxb9l6AoMTY3gCTKvvkB+mq5B9BcRm2qQ+XOrOKxV5c44o7jK+eN\n" + + "W/fnZkSxYsqZW4fEFU1SkNTiU/vxT0ZeHs6nHD/xAoGAOIqaqQnJfGj/wLo3Z9o5\n" + + "/Oxu1zTPGZC6SqpdygCjlQ0kQ8Bp0LV7nL06/VCHAHI2lF12xApRnFk7GY3xyqK8\n" + + "nYxeRASCj3GGmLupGshtfCtDBeysE2h7kj3Bo0d6g1Ye+j8BUZuZaZm6WNlo7cgE\n" + + "NLHn1k0IpmXFOiFa1Y1D6Bc=\n" + + "-----END PRIVATE KEY-----"; + + private Pkcs7HashSignService service; + + @BeforeEach + void setup(){ + System.setProperty("crypto.key.cert", testCert); + System.setProperty("crypto.key.private", testPrivateKey); + service = new Pkcs7HashSignServiceImpl(); + } + @Test + void shouldAssertEncoding() throws IOException { + try (FileInputStream fis = new FileInputStream(Path.of("src/test/resources/signTest.pdf").toFile())) { + byte[] result = service.sign(fis); + Assertions.assertNotNull(result); + checkPkcs7HashSign(Base64.getEncoder().encodeToString(result)); + } + } + + protected void checkPkcs7HashSign(String result) { + String expectedPrefix = "MIAGCSqGSIb3DQEHAqCAMIACAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBoIAwggOfMIICh6ADAgECAhQnz"; + Assertions.assertTrue( + result.startsWith(expectedPrefix), + String.format("Prefix unexpected:\n%s\n%s", result, expectedPrefix) + ); + } +} diff --git a/libs/onboarding-sdk-crypto/src/test/resources/signTest.pdf b/libs/onboarding-sdk-crypto/src/test/resources/signTest.pdf new file mode 100644 index 000000000..19d5aefec Binary files /dev/null and b/libs/onboarding-sdk-crypto/src/test/resources/signTest.pdf differ diff --git a/libs/onboarding-sdk-pom/pom.xml b/libs/onboarding-sdk-pom/pom.xml index 9af27883b..0c0ea2667 100644 --- a/libs/onboarding-sdk-pom/pom.xml +++ b/libs/onboarding-sdk-pom/pom.xml @@ -15,6 +15,8 @@ 17 17 + 5.7.2 + 5.7.0 @@ -29,8 +31,30 @@ ../onboarding-sdk-azure-storage ../onboarding-sdk-common ../onboarding-sdk-product + ../onboarding-sdk-crypto + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + diff --git a/libs/onboarding-sdk-product/pom.xml b/libs/onboarding-sdk-product/pom.xml index d65e1c1b7..1b29b83e9 100644 --- a/libs/onboarding-sdk-product/pom.xml +++ b/libs/onboarding-sdk-product/pom.xml @@ -12,7 +12,6 @@ 2.15.2 - 5.7.2 @@ -30,19 +29,6 @@ ${project.version} - - org.junit.jupiter - junit-jupiter-api - ${junit-jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit-jupiter.version} - test - - diff --git a/libs/pom.xml b/libs/pom.xml index 5a58db456..f5c434c12 100644 --- a/libs/pom.xml +++ b/libs/pom.xml @@ -17,6 +17,18 @@ + + + + io.quarkus.platform + quarkus-bom + 3.4.2 + pom + import + + + + diff --git a/test-coverage/pom.xml b/test-coverage/pom.xml index 72776ef51..a1ba02965 100644 --- a/test-coverage/pom.xml +++ b/test-coverage/pom.xml @@ -89,6 +89,11 @@ onboarding-sdk-azure-storage 0.1.0 + + it.pagopa.selfcare + onboarding-sdk-crypto + 0.1.0 +