From 779bdbb3c10d4b2f497e0eb332c201d5fd01dec1 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 3 Mar 2016 14:36:21 +0100 Subject: [PATCH] [New] Added new XmlDomSigner with tests --- .../eitchnet/utils/helper/XmlDomSigner.java | 245 ++++++++++++++++++ .../utils/helper/XmlSignHelperTest.java | 185 +++++++++++++ src/test/resources/SignedXmlFile.xml | 20 ++ .../resources/SignedXmlFileWithNamespaces.xml | 20 ++ src/test/resources/test.jks | Bin 0 -> 2263 bytes 5 files changed, 470 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java create mode 100644 src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java create mode 100644 src/test/resources/SignedXmlFile.xml create mode 100644 src/test/resources/SignedXmlFileWithNamespaces.xml create mode 100644 src/test/resources/test.jks diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java b/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java new file mode 100644 index 000000000..bb0bf014e --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java @@ -0,0 +1,245 @@ +package ch.eitchnet.utils.helper; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyStore; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; +import javax.xml.crypto.dsig.keyinfo.X509Data; +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public class XmlDomSigner { + + private static final Logger logger = LoggerFactory.getLogger(XmlDomSigner.class); + + private PrivateKeyEntry keyEntry; + private X509Certificate cert; + + public XmlDomSigner(File keyStorePath, String alias, char[] password) { + try { + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(new FileInputStream(keyStorePath), password); + this.keyEntry = (PrivateKeyEntry) ks.getEntry(ks.aliases().nextElement(), + new KeyStore.PasswordProtection(password)); + + this.cert = (X509Certificate) this.keyEntry.getCertificate(); + + } catch (Exception e) { + throw new RuntimeException( + "Failed to read certificate and private key from keystore " + keyStorePath + " and alias " + alias); + } + } + + public void sign(Document document) throws RuntimeException { + + try { + + String id = "Signed_" + UUID.randomUUID().toString(); + Element rootElement = document.getDocumentElement(); + rootElement.setAttribute("ID", id); + rootElement.setIdAttribute("ID", true); + + // Create a DOM XMLSignatureFactory that will be used to + // generate the enveloped signature. + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + + // Create a Reference to the enveloped document (in this case, + // you are signing the whole document, so a URI of "" signifies + // that, and also specify the SHA1 digest algorithm and + // the ENVELOPED Transform. + List transforms = new ArrayList<>(); + transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null)); + transforms.add(fac.newTransform(CanonicalizationMethod.EXCLUSIVE, (TransformParameterSpec) null)); + DigestMethod digestMethod = fac.newDigestMethod(DigestMethod.SHA1, null); + Reference ref = fac.newReference("#" + id, digestMethod, transforms, null, null); + //Reference ref = fac.newReference("", digestMethod, transforms, null, null); + + // Create the SignedInfo. + SignedInfo signedInfo = fac.newSignedInfo( + fac.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null), // + fac.newSignatureMethod(SignatureMethod.RSA_SHA1, null), // + Collections.singletonList(ref)); + + // Load the KeyStore and get the signing key and certificate. + + // Create the KeyInfo containing the X509Data. + KeyInfoFactory kif = fac.getKeyInfoFactory(); + List x509Content = new ArrayList<>(); + x509Content.add(this.cert.getSubjectX500Principal().getName()); + x509Content.add(this.cert); + X509Data xd = kif.newX509Data(x509Content); + KeyInfo keyInfo = kif.newKeyInfo(Collections.singletonList(xd)); + + // Create a DOMSignContext and specify the RSA PrivateKey and + // location of the resulting XMLSignature's parent element. + DOMSignContext dsc = new DOMSignContext(this.keyEntry.getPrivateKey(), rootElement); + //dsc.setDefaultNamespacePrefix("samlp"); + dsc.putNamespacePrefix(XMLSignature.XMLNS, "ds"); + + // Create the XMLSignature, but don't sign it yet. + XMLSignature signature = fac.newXMLSignature(signedInfo, keyInfo); + + // Marshal, generate, and sign the enveloped signature. + signature.sign(dsc); + + } catch (Exception e) { + throw new RuntimeException("Failed to sign document", e); + } + } + + public void validate(Document doc) throws RuntimeException { + + try { + + // Create a DOM XMLSignatureFactory that will be used to + // generate the enveloped signature. + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + + // Find Signature element. + NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + if (nl.getLength() == 0) { + throw new Exception("Cannot find Signature element!"); + } else if (nl.getLength() > 1) { + throw new Exception("Found multiple Signature elements!"); + } + + // Load the KeyStore and get the signing key and certificate. + PublicKey publicKey = this.keyEntry.getCertificate().getPublicKey(); + + // Create a DOMValidateContext and specify a KeySelector + // and document context. + Node signatureNode = nl.item(0); + DOMValidateContext valContext = new DOMValidateContext(publicKey, signatureNode); + valContext.setDefaultNamespacePrefix("samlp"); + valContext.putNamespacePrefix(XMLSignature.XMLNS, "ds"); + valContext.putNamespacePrefix("urn:oasis:names:tc:SAML:2.0:protocol", "samlp"); + valContext.putNamespacePrefix("urn:oasis:names:tc:SAML:2.0:assertion", "saml"); + valContext.setIdAttributeNS(doc.getDocumentElement(), null, "ID"); + + // Unmarshal the XMLSignature. + valContext.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE); + XMLSignature signature = fac.unmarshalXMLSignature(valContext); + + // Validate the XMLSignature. + boolean coreValidity = signature.validate(valContext); + + // Check core validation status. + if (!coreValidity) { + logger.error("Signature failed core validation"); + boolean sv = signature.getSignatureValue().validate(valContext); + logger.error("signature validation status: " + sv); + if (!sv) { + // Check the validation status of each Reference. + Iterator i = signature.getSignedInfo().getReferences().iterator(); + for (int j = 0; i.hasNext(); j++) { + boolean refValid = ((Reference) i.next()).validate(valContext); + logger.error("ref[" + j + "] validity status: " + refValid); + } + } + throw new RuntimeException("Uh-oh validation, failed!"); + } + + } catch (Exception e) { + if (e instanceof RuntimeException) + throw (RuntimeException) e; + throw new RuntimeException("Failed to validate document", e); + } + } + + public static byte[] transformToBytes(Document doc) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.transform(new DOMSource(doc), new StreamResult(out)); + return out.toByteArray(); + } catch (TransformerFactoryConfigurationError | TransformerException e) { + throw new RuntimeException("Failed to transform document to bytes!", e); + } + } + + public static void writeTo(Document doc, File file) { + try { + writeTo(doc, new FileOutputStream(file)); + } catch (FileNotFoundException e) { + throw new RuntimeException("Failed to write document to " + file.getAbsolutePath(), e); + } + } + + public static void writeTo(Document doc, OutputStream out) { + try { + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.transform(new DOMSource(doc), new StreamResult(out)); + } catch (Exception e) { + throw new RuntimeException("Failed to write document to output stream!", e); + } + } + + public static Document parse(byte[] bytes) { + return parse(new ByteArrayInputStream(bytes)); + } + + public static Document parse(File signedXmlFile) { + try { + return parse(new FileInputStream(signedXmlFile)); + } catch (Exception e) { + throw new RuntimeException("Failed to parse signed file at " + signedXmlFile.getAbsolutePath(), e); + } + } + + public static Document parse(InputStream in) { + + Document doc; + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + doc = dbf.newDocumentBuilder().parse(in); + } catch (Exception e) { + throw new RuntimeException("Failed to parse input stream", e); + } + + return doc; + } +} diff --git a/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java b/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java new file mode 100644 index 000000000..561413319 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java @@ -0,0 +1,185 @@ +package ch.eitchnet.utils.helper; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.TimeZone; + +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +public class XmlSignHelperTest { + + private static XmlDomSigner helper; + + @BeforeClass + public static void beforeClass() { + helper = new XmlDomSigner(new File("src/test/resources/test.jks"), "client", "changeit".toCharArray()); + } + + @Test + public void shouldSign() { + Document document = createDoc(); + helper.sign(document); + + assertSignatureElemExists(document); + } + + @Test + public void shouldSignWithNamespaces() { + + Document document = createDocWithNamespaces(); + + // hack for signing with namespaces problem + document = XmlDomSigner.parse(XmlDomSigner.transformToBytes(document)); + + helper.sign(document); + + assertSignatureElemExists(document); + } + + private void assertSignatureElemExists(Document document) { + NodeList nl = document.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + assertEquals("Expected exactly one Signature element!", 1, nl.getLength()); + } + + @Test + public void shouldValidate() { + + File signedXmlFile = new File("src/test/resources/SignedXmlFile.xml"); + Document document = XmlDomSigner.parse(signedXmlFile); + helper.validate(document); + } + + @Test + public void shouldValidateWithNamespaces() { + + File signedXmlFile = new File("src/test/resources/SignedXmlFileWithNamespaces.xml"); + Document document = XmlDomSigner.parse(signedXmlFile); + helper.validate(document); + } + + @Test + public void shouldSignAndValidate() { + + Document document = createDoc(); + + helper.sign(document); + helper.validate(document); + } + + @Test + public void shouldSignAndValidateWithNamespaces() { + + Document document = createDocWithNamespaces(); + + // hack for signing with namespaces problem + document = XmlDomSigner.parse(XmlDomSigner.transformToBytes(document)); + + helper.sign(document); + helper.validate(document); + } + + public static Document createDoc() { + + String issuer = "test"; + String destination = "test"; + String assertionConsumerServiceUrl = "test"; + Calendar issueInstant = Calendar.getInstance(); + + // create dates + SimpleDateFormat simpleDf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + simpleDf.setTimeZone(TimeZone.getTimeZone("UTC")); + String issueInstantS = simpleDf.format(issueInstant.getTime()); + String notBeforeS = issueInstantS; + issueInstant.add(Calendar.SECOND, 10); + String notOnOrAfterS = simpleDf.format(issueInstant.getTime()); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder docBuilder; + try { + docBuilder = dbf.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new RuntimeException("Failed to configure document builder!", e); + } + Document doc = docBuilder.newDocument(); + + Element authnReqE = doc.createElement("AuthnRequest"); + authnReqE.setAttribute("Version", "2.0"); + authnReqE.setAttribute("IssueInstant", issueInstantS); + authnReqE.setAttribute("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + authnReqE.setAttribute("AssertionConsumerServiceURL", assertionConsumerServiceUrl); + authnReqE.setAttribute("Destination", destination); + doc.appendChild(authnReqE); + + Element issuerE = doc.createElement("Issuer"); + issuerE.setTextContent(issuer); + authnReqE.appendChild(issuerE); + + Element conditionsE = doc.createElement("Conditions"); + conditionsE.setAttribute("NotBefore", notBeforeS); + conditionsE.setAttribute("NotOnOrAfter", notOnOrAfterS); + authnReqE.appendChild(conditionsE); + + return doc; + } + + public static Document createDocWithNamespaces() { + + String issuer = "test"; + String destination = "test"; + String assertionConsumerServiceUrl = "test"; + Calendar issueInstant = Calendar.getInstance(); + + // create dates + SimpleDateFormat simpleDf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + simpleDf.setTimeZone(TimeZone.getTimeZone("UTC")); + String issueInstantS = simpleDf.format(issueInstant.getTime()); + String notBeforeS = issueInstantS; + issueInstant.add(Calendar.SECOND, 10); + String notOnOrAfterS = simpleDf.format(issueInstant.getTime()); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder docBuilder; + try { + docBuilder = dbf.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new RuntimeException("Failed to configure document builder!", e); + } + Document doc = docBuilder.newDocument(); + + Element authnReqE = doc.createElementNS("urn:oasis:names:tc:SAML:2.0:protocol", "AuthnRequest"); + authnReqE.setPrefix("samlp"); + authnReqE.setAttribute("Version", "2.0"); + authnReqE.setAttribute("IssueInstant", issueInstantS); + authnReqE.setAttribute("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + authnReqE.setAttribute("AssertionConsumerServiceURL", assertionConsumerServiceUrl); + authnReqE.setAttribute("Destination", destination); + doc.appendChild(authnReqE); + + Element issuerE = doc.createElementNS("urn:oasis:names:tc:SAML:2.0:assertion", "Issuer"); + issuerE.setPrefix("saml"); + issuerE.setTextContent(issuer); + authnReqE.appendChild(issuerE); + + Element conditionsE = doc.createElementNS("urn:oasis:names:tc:SAML:2.0:assertion", "Conditions"); + conditionsE.setPrefix("saml"); + conditionsE.setAttribute("NotBefore", notBeforeS); + conditionsE.setAttribute("NotOnOrAfter", notOnOrAfterS); + authnReqE.appendChild(conditionsE); + + return doc; + } +} diff --git a/src/test/resources/SignedXmlFile.xml b/src/test/resources/SignedXmlFile.xml new file mode 100644 index 000000000..63197c399 --- /dev/null +++ b/src/test/resources/SignedXmlFile.xml @@ -0,0 +1,20 @@ +test6bxgcmypqGyvyQFgEqdOk1zLTOg=RAWnchzzSHwi84ZJcog6OnkYrGx7rBGBHDsysn1lmP05+AydKzcK7Jw3kbpkGnaaz1DIV/8A/hhA +UmOjwAl8RNygtziXuS5fZfvDUidhPugv2EUKeUkH7CwMkLSB5TONC+AS8eEhfyZbl/4GYMd4Jcqx +OQJgBRMNT6zfybFY+wfJDceFUqCCmyXFgcGBmtSjJqQivwH4B8k1ui49hO67ItCBcCo0aKpqoIxF +UA2IZCDvmrdR/qCq/oA9ssjUzpC+yJMvwPtZ6LDdoWt+MDzDBkCKA6lKUqtU51rZ8GwoRLve8FXH +vCG/ZiqxKn4JwBQL2DiFuZVXhrrMvLpYyi4ZRA==CN=test,OU=ch.eitchnet.utils,O=ch.eitchnet,L=Solothurn,ST=Solothurn,C=CHMIIDizCCAnOgAwIBAgIEPPVdzDANBgkqhkiG9w0BAQsFADB2MQswCQYDVQQGEwJDSDESMBAGA1UE +CBMJU29sb3RodXJuMRIwEAYDVQQHEwlTb2xvdGh1cm4xFDASBgNVBAoTC2NoLmVpdGNobmV0MRow +GAYDVQQLExFjaC5laXRjaG5ldC51dGlsczENMAsGA1UEAxMEdGVzdDAeFw0xNjAzMDMxMzEwMTRa +Fw0xNjA2MDExMzEwMTRaMHYxCzAJBgNVBAYTAkNIMRIwEAYDVQQIEwlTb2xvdGh1cm4xEjAQBgNV +BAcTCVNvbG90aHVybjEUMBIGA1UEChMLY2guZWl0Y2huZXQxGjAYBgNVBAsTEWNoLmVpdGNobmV0 +LnV0aWxzMQ0wCwYDVQQDEwR0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxW8E +F/odm00xQTRQ95b9RtpmiQo3OQfpkE344vyp48BpOHMm8Q+sjTHde6hGUQQ3FSp6jUZtJBmuXDp+ +hFa4zNp1vleASqmUw4VCZZN1BgHx6coWA1zw/GWyCOFHvr+JTP6/LAfemudOnQVWKx6/NkMEgjNm +OR3qsbfj1km/VlfnIAOG9SvFvydp+Jan7y+nIKllEEXpcQFeiWouvC572aptu9k9qe43mRoQHJ96 +IngJHLONi27SBsF31ipvoHjkYEaTYfv8Izf5wt7h+8tZipFHu0+5r7LbZDRhSWzopC5KZakgVgLG +0JYEzcLotOCf9hmDDZZAAv3OyLoMqN8F0QIDAQABoyEwHzAdBgNVHQ4EFgQUd38xdRA7VcrWcjmz +CYmbBMD4SaUwDQYJKoZIhvcNAQELBQADggEBACqDrXrrYG2sfFBRTIQVni291q8tDqJ1etim1fND +s9ZdYa2oKTaLjMswdlE5hVXtEvRrN+XX3TIAK0lfiDwF4E4JBDww4a3SefEbwPvx110WN6CTE1NI +P6IPCUB7e5QOlg4uKAJoZfnY6HboRiHbeOQxIeis3Q9XpqQSYrO4/NzxFt66m48BHLqf8Hwi90GY +VYMljqr+hHvUTQWGzFD3NKr9Fq6yO2GcHGc5ifmLjwoz2EDAsSubrccbN+RQRRg3II6gFxyL9PYN +HgkGjqdg3v8TiWRxWAFL2MrgNLRzOX9Sl7NMFo6JwiizfLWTgcxZZVIkcU1ZP4heXi5iKUzgsJM= \ No newline at end of file diff --git a/src/test/resources/SignedXmlFileWithNamespaces.xml b/src/test/resources/SignedXmlFileWithNamespaces.xml new file mode 100644 index 000000000..012c1783b --- /dev/null +++ b/src/test/resources/SignedXmlFileWithNamespaces.xml @@ -0,0 +1,20 @@ +testKseWAs8E4H1ZGAbyl2EnlZ3RiG4=KhSDJRxo1u7eNVy1swN5RqA+37oCCeyY8QNCtT1RFz8UVZFqmXGiurscbctKA+tiYSekW4OkxEg9 +Nv03OGJcYlksdZ5CCGlsioac+NY/z2QngtlDaFudKIHwj9yZ9zMdiKT/4kdwnUQP+p9tzYV9GeA9 +gesLOielMdj382XoFQ/CIbrJevE4vpn9FSitbwHXV4kZ3/NxlBPYIgiM9yiTTT0NafFENTS38U+P +k1tL32FcDfHytWN6Twl2ZbHrRYltba/ncxaqkauMA37r9v2f+HS+hXXNluLTazRzAxnhSaetjPOt +GFP/nkG0TcRbiZFTP3YwIHeo94v2d/fs6rKHiQ==CN=test,OU=ch.eitchnet.utils,O=ch.eitchnet,L=Solothurn,ST=Solothurn,C=CHMIIDizCCAnOgAwIBAgIEPPVdzDANBgkqhkiG9w0BAQsFADB2MQswCQYDVQQGEwJDSDESMBAGA1UE +CBMJU29sb3RodXJuMRIwEAYDVQQHEwlTb2xvdGh1cm4xFDASBgNVBAoTC2NoLmVpdGNobmV0MRow +GAYDVQQLExFjaC5laXRjaG5ldC51dGlsczENMAsGA1UEAxMEdGVzdDAeFw0xNjAzMDMxMzEwMTRa +Fw0xNjA2MDExMzEwMTRaMHYxCzAJBgNVBAYTAkNIMRIwEAYDVQQIEwlTb2xvdGh1cm4xEjAQBgNV +BAcTCVNvbG90aHVybjEUMBIGA1UEChMLY2guZWl0Y2huZXQxGjAYBgNVBAsTEWNoLmVpdGNobmV0 +LnV0aWxzMQ0wCwYDVQQDEwR0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxW8E +F/odm00xQTRQ95b9RtpmiQo3OQfpkE344vyp48BpOHMm8Q+sjTHde6hGUQQ3FSp6jUZtJBmuXDp+ +hFa4zNp1vleASqmUw4VCZZN1BgHx6coWA1zw/GWyCOFHvr+JTP6/LAfemudOnQVWKx6/NkMEgjNm +OR3qsbfj1km/VlfnIAOG9SvFvydp+Jan7y+nIKllEEXpcQFeiWouvC572aptu9k9qe43mRoQHJ96 +IngJHLONi27SBsF31ipvoHjkYEaTYfv8Izf5wt7h+8tZipFHu0+5r7LbZDRhSWzopC5KZakgVgLG +0JYEzcLotOCf9hmDDZZAAv3OyLoMqN8F0QIDAQABoyEwHzAdBgNVHQ4EFgQUd38xdRA7VcrWcjmz +CYmbBMD4SaUwDQYJKoZIhvcNAQELBQADggEBACqDrXrrYG2sfFBRTIQVni291q8tDqJ1etim1fND +s9ZdYa2oKTaLjMswdlE5hVXtEvRrN+XX3TIAK0lfiDwF4E4JBDww4a3SefEbwPvx110WN6CTE1NI +P6IPCUB7e5QOlg4uKAJoZfnY6HboRiHbeOQxIeis3Q9XpqQSYrO4/NzxFt66m48BHLqf8Hwi90GY +VYMljqr+hHvUTQWGzFD3NKr9Fq6yO2GcHGc5ifmLjwoz2EDAsSubrccbN+RQRRg3II6gFxyL9PYN +HgkGjqdg3v8TiWRxWAFL2MrgNLRzOX9Sl7NMFo6JwiizfLWTgcxZZVIkcU1ZP4heXi5iKUzgsJM= \ No newline at end of file diff --git a/src/test/resources/test.jks b/src/test/resources/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..0e44e5f08687cfb55bec62c63c664b5fccf208ca GIT binary patch literal 2263 zcmc&#_fwOL63v&U0YWd*tAdpH#Q-rBDM}SYz=UGx1VJg%yGSU~LBUv1`c*&>X%~>L zQiKS|RU}9Ur3nZky*#}0-i-gj`{Dd>&hG5&oZZ>oU)*1WKp@aV0RIB%H zY7dLvK_CbKq{8<=RwR=g5`Y7;P!0e=1HFV)UuN=Q`v3^WlaBI5Jh=+(`p_v-!&!4Ozorij~4dw49uyE zNfDCQ2UTavy5^*ke3IHr#sk7o2ig!rocPfc47K!gOEUS+qP6DKwzOM~&S2u)xSd~A zBMwcCVOns;XJMOpq<;j2uG7cuscHv%dM*(iZ;gDLl%%%&h&BUy``X&$z>%MN5{OwS zxwx;R;%aMXB$m-b}&j-XhUEq7Jq|>F}yBBKqRdE*FY2-i-8Q^d=B~f{T z`NJ)UtF4$wqR9CKw!u?V#oEjNKp&K<@HEN&U4n+wPd&18YU7sSu-KK>;;(1goN6^? zWZ}&Z>ifP2#0#z5qtbQw}YJJ{{J41!UVAy6o=|RbslOPz{@4X|V+7@^a=>MQ0HwS>Hu=f5E3} z9NPkhUm`z5JB*OMb6p7UXe@zhO3YczEbqJ;bst?wrMZTrH-HD(utCSW(8U-d$FqXx z**(3A4ZoQ;i{=*oG8_woo2zIS2V8!#2fFdEnpou6RV_(WX>Rd9FKD~i+0Rwq;@oS3 z?^qg+pRjEaZrCL`@j zWV$TD%{?A3`83IRBEYtbZ9R+Y+6_XIZ(?3Ck4gWp+@+c+rss4~mvFy*ZioV3qV+wR zGG(yPVwmUFyCHHMN2P)>5W|h~+579va6cf=`>3Kpf_D4kWz))M>_g_lDro^38{BkscBW3%|464ai0pJ)4ubIZ`1ffAymj`SsT+H4VJtETb4VqkuEdS zCcHss$PuAyi7Kc_`OdFOrpoLO8&WgPc$OIgJB}qCa_4B`SpybO_28b-at~T~dR5qZ zRoyKtJanNev-^r`*M#y5qvakABl^8BQOWAJf8Gn{UvfNVZAq>p>N_e{YrGK39paAU zUGwLycT|#^n{(BKVhJbS8jNL&UMXqkBNbd}vy{J&W4#mcX?b8?BTfn7?z+2)(Chd{ zCoR9)3$g9FajPe-MXUBfo=Oq`8!$elSl+)A?`l5Ku0&)Fz-APG$b*pldC7i4=Vs3`>ir_ar~iT{QcbAeX(ee z`F9M)1^1))`hg;RXsj}b194a!7Q`xA|4WrY?0@P1tuHDd@ux+H4+KU9*dQPkV1iNs z0Mh0K=i3&}G{$NvUfQ7T=?=NXGvNtG=F*HeCwGgcTHMa~o>*ZkNyd(Z7V4V9@jS92 z$+{j={4dBVH={|l^dbKyi(7_88E;~=De3-5U}dh07e@ZEOQ|?Iq1V(LZ?xYme`NI0 z%*7`NlAK7h@;Nva=Ry#kFRz&zFlZ)O%!t8af628qOS^5-o-LnxCRRjY*O~JMY~!zC z8Ze=ZVvl-;deIX8p#Zy3c8Ej}N~kh9(X$`f8aN>9l@l~=ubb|$vnz?;dObR^(`}V- zN3Y(buB_sVv!a86`)mQmkWwT@g0}b3;O}40R*h$`^GBg+n$W$T&Q~mjV~7t>7ytqC z#X(U}_;8%UtZ-g9cVGn8pIw#MHQ+<2M8#*qTQ&{;`qRY2Jv%fJfXGIbhAh~7l!RR} zHHzlRQfM3~Q((>W4;g&=>F2r10UL+X!jsC0N!?(8DItcq$hmeM|Mm08X^5PGU7Q+X z{2~gj22Par2d@aW?5upY;l<~qbD8UF#Hiyb#rx5rAB_>Q^h+Cx#e2LjDpVaF z3tc6|ZzZNO;RZEZ%H=XkI|T95mvoNd#Zq$kgc8@*(IO~h$}{`X1Fm>yZ%g39VAr@} zl`kQ}?0%&YZ%X`YnaZ&0^hmlD#Z1cE*h(YL))wP<(rEl;dWp<6l)BDA#CLh|u?Z7( JGST@q<3HE1+SUL7 literal 0 HcmV?d00001