From 58c5c7b17a0e9c7e3e2e11143f352c76baddeb39 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 14:06:31 +0200 Subject: [PATCH] Test with a static test cert and keystore instead of generating a new one each run --- pom.xml | 5 - .../test/https/SelfSignedCertificates.java | 163 ------------------ .../SelfSignedCertificatesException.java | 38 ---- .../jvnet/hudson/test/RealJenkinsRule.java | 47 +++-- .../keystore/generate_self_signed_cert.sh | 47 +++++ src/main/resources/https/test-cert.pem | 24 +++ src/main/resources/https/test-keystore.p12 | Bin 0 -> 2771 bytes src/src/main/resources/https/test-cert.pem | 24 +++ .../main/resources/https/test-keystore.p12 | Bin 0 -> 2787 bytes 9 files changed, 118 insertions(+), 230 deletions(-) delete mode 100644 src/main/java/jenkins/test/https/SelfSignedCertificates.java delete mode 100644 src/main/java/jenkins/test/https/SelfSignedCertificatesException.java create mode 100755 src/main/keystore/generate_self_signed_cert.sh create mode 100644 src/main/resources/https/test-cert.pem create mode 100644 src/main/resources/https/test-keystore.p12 create mode 100644 src/src/main/resources/https/test-cert.pem create mode 100644 src/src/main/resources/https/test-keystore.p12 diff --git a/pom.xml b/pom.xml index 253863e23..d57017f27 100644 --- a/pom.xml +++ b/pom.xml @@ -212,11 +212,6 @@ THE SOFTWARE. jmh-generator-annprocess ${jmh.version} - - org.bouncycastle - bcpkix-jdk18on - 1.78.1 - org.jenkins-ci test-annotations diff --git a/src/main/java/jenkins/test/https/SelfSignedCertificates.java b/src/main/java/jenkins/test/https/SelfSignedCertificates.java deleted file mode 100644 index 7481dce55..000000000 --- a/src/main/java/jenkins/test/https/SelfSignedCertificates.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * The MIT License - * - * Copyright 2024 CloudBees, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package jenkins.test.https; - -import edu.umd.cs.findbugs.annotations.NonNull; -import java.math.BigInteger; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.Security; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.time.ZoneId; -import java.util.Date; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.BasicConstraints; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.GeneralName; -import org.bouncycastle.asn1.x509.GeneralNames; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.cert.CertIOException; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; - -/** - * A utility class to generate self-signed certificates. - *

Use static method {@link #createRootCAs()} to generate a root CA. - *

Then you can generate user certificates for a given DNS name signed by the intermediate CA. - * @param root The root CA certificate and key pair. - * @param intermediate The intermediate CA certificate and key pair. - * - * @see #createRootCAs() - * @see #createUserCert(String, CertificateKeyPair) - */ -public record SelfSignedCertificates(CertificateKeyPair root, CertificateKeyPair intermediate) { - private static final SecureRandom RANDOM = new SecureRandom(); - private static final String CERTIFICATE_ALGORITHM = "RSA"; - private static final int CERTIFICATE_BITS = 2048; - private static final String ROOT_DN = "CN=Root"; - - static { - // adds the Bouncy castle provider to java security - Security.addProvider(new BouncyCastleProvider()); - } - - @NonNull - public static SelfSignedCertificates createRootCAs() { - try { - // create root CA self-signed. - var rootKeyPair = generateKeyPair(); - var rootBuilder = new JcaX509v3CertificateBuilder( - new X500Name(ROOT_DN), - generateSerialNumber(), - Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()), - Date.from(LocalDate.now() - .plusDays(4) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant()), - new X500Name(ROOT_DN), - SubjectPublicKeyInfo.getInstance(rootKeyPair.getPublic().getEncoded())); - rootBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); - rootBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true)); - var root = new CertificateKeyPair(rootKeyPair, new JcaX509CertificateConverter().getCertificate(rootBuilder.build(newContentSigner(rootKeyPair)))); - - // create Intermediate CA cert signed by Root CA - var intermediateKeyPair = generateKeyPair(); - var intermediateBuilder = new JcaX509v3CertificateBuilder( - root.certificate(), - generateSerialNumber(), - Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()), - Date.from(LocalDate.now() - .plusDays(2) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant()), - new X500Name("CN=Intermediate"), - intermediateKeyPair.getPublic()); - intermediateBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); - intermediateBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true)); - var intermediate = new CertificateKeyPair(intermediateKeyPair, new JcaX509CertificateConverter().getCertificate(intermediateBuilder.build(newContentSigner(root.keyPair())))); - return new SelfSignedCertificates(root, intermediate); - } catch (OperatorCreationException | CertificateException | CertIOException | NoSuchAlgorithmException e) { - throw new SelfSignedCertificatesException(e); - } - } - - private static ContentSigner newContentSigner(KeyPair keyPair) throws OperatorCreationException { - return new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate()); - } - - @NonNull - static BigInteger generateSerialNumber() { - return BigInteger.valueOf(RANDOM.nextInt()); - } - - /** - * Generate a user certificate signed by the intermediate CA. - * @param dnsName The DNS name to use in the certificate. - * @param issuer The intermediate CA to sign the certificate. - * @return The user certificate and key pair. - */ - public static CertificateKeyPair createUserCert(String dnsName, CertificateKeyPair issuer) { - try { - var keyPair = generateKeyPair(); - // create end user cert signed by Intermediate CA - var builder = new JcaX509v3CertificateBuilder( - issuer.certificate(), - generateSerialNumber(), - Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()), - Date.from(LocalDate.now() - .plusDays(1) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant()), - new X500Name("CN=endUserCert"), - keyPair.getPublic()); - builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature)); - builder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false)); - builder.addExtension(Extension.subjectAlternativeName, false, GeneralNames.getInstance(new DERSequence(new GeneralName(GeneralName.dNSName, dnsName)))); - return new CertificateKeyPair( - keyPair, - new JcaX509CertificateConverter().getCertificate(builder.build(newContentSigner(issuer.keyPair())))); - } catch (OperatorCreationException | CertificateException | CertIOException | NoSuchAlgorithmException e) { - throw new SelfSignedCertificatesException(e); - } - } - - private static KeyPair generateKeyPair() throws NoSuchAlgorithmException { - var keyPairGenerator = KeyPairGenerator.getInstance(CERTIFICATE_ALGORITHM); - keyPairGenerator.initialize(CERTIFICATE_BITS, RANDOM); - return keyPairGenerator.generateKeyPair(); - } - - public record CertificateKeyPair(KeyPair keyPair, X509Certificate certificate) {} -} diff --git a/src/main/java/jenkins/test/https/SelfSignedCertificatesException.java b/src/main/java/jenkins/test/https/SelfSignedCertificatesException.java deleted file mode 100644 index f05858941..000000000 --- a/src/main/java/jenkins/test/https/SelfSignedCertificatesException.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * The MIT License - * - * Copyright 2024 CloudBees, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package jenkins.test.https; - -/** - * Raised when something went wrong when generating self-signed certificates. - */ -public class SelfSignedCertificatesException extends RuntimeException { - public SelfSignedCertificatesException(Throwable cause) { - super(cause); - } - - public SelfSignedCertificatesException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 963ab1994..de0410b2e 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -67,11 +67,10 @@ import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; @@ -112,8 +111,10 @@ import io.jenkins.test.fips.FIPSTestBundleProvider; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; +import jenkins.test.https.KeyStoreManager; import jenkins.util.Timer; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.htmlunit.WebClient; import org.junit.AssumptionViolatedException; import org.junit.rules.DisableOnDebug; @@ -122,8 +123,6 @@ import org.junit.rules.Timeout; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import jenkins.test.https.KeyStoreManager; -import jenkins.test.https.SelfSignedCertificates; import org.jvnet.hudson.test.recipes.LocalData; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; @@ -765,54 +764,54 @@ public URL getUrl() throws MalformedURLException { /** * Sets up HTTPS for the current instance, and disables plain HTTP. - * This generates a self-signed certificate for localhost. The corresponding root CA that needs to be trusted by HTTP client can be obtained using {@link #getRootCA()}. + * This generates a self-signed certificate for localhost. The corresponding root CA that needs to be trusted by HTTP client can be obtained using {@link #getRootCA()}. * * @return the current instance * @see #createWebClient() */ public RealJenkinsRule https() { - return https("localhost"); + try { + var keyStorePath = tmp.allocate().toPath().resolve("test-keystore.p12"); + IOUtils.copy(getClass().getResource("/https/test-keystore.p12"), keyStorePath.toFile()); + var keyStoreManager = new KeyStoreManager(keyStorePath, "changeit"); + try (var is = getClass().getResourceAsStream("/https/test-cert.pem")) { + var cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); + https("localhost", keyStoreManager, cert); + } + } catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | IOException e) { + throw new RuntimeException(e); + } + return this; } - /** * Sets up HTTPS for the current instance, and disables plain HTTP. - * This generates a self-signed certificate for the given host name. The corresponding root CA that needs to be trusted by HTTP client can be obtained using {@link #getRootCA()}. *

* You don't need to call {@link #withHost(String)} when calling this method. * * @param host the host name to use in the certificate + * @param keyStoreManager a key store manager containing the key and certificate to use for HTTPS. It needs to be valid for the given host + * @param rootCA the certificate that needs to be trusted by callers. * @return the current instance * @see #createWebClient() * @see #withHost(String) */ - public RealJenkinsRule https(@NonNull String host) { + public RealJenkinsRule https(@NonNull String host, @NonNull KeyStoreManager keyStoreManager, @NonNull X509Certificate rootCA) { this.host = host; this.https = true; + this.keyStoreManager = keyStoreManager; try { - // TODO remove password once we have https://github.com/jenkinsci/winstone/pull/417 - var keyStoreManager = new KeyStoreManager(createTempDirectory("keystore").resolve("keyStore.p12"), token); - var rootCAs = SelfSignedCertificates.createRootCAs(); - var userCert = SelfSignedCertificates.createUserCert(host, rootCAs.intermediate()); - keyStoreManager.setCertificateEntry(host, rootCAs.root().certificate()); - keyStoreManager.setKeyEntry(host, userCert.keyPair().getPrivate(), new Certificate[]{ - userCert.certificate(), - rootCAs.intermediate().certificate(), - rootCAs.root().certificate() - }); - keyStoreManager.save(); - this.rootCA = rootCAs.root().certificate(); - this.keyStoreManager = keyStoreManager; this.sslSocketFactory = keyStoreManager.buildClientSSLContext().getSocketFactory(); - } catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException | + } catch (NoSuchAlgorithmException | KeyManagementException | CertificateException | KeyStoreException | IOException e) { throw new RuntimeException(e); } + this.rootCA = rootCA; return this; } /** - * @return the current autogenerated root CA or null if {@link #https(String)} has not been called. + * @return the current autogenerated root CA or null if {@link #https()} has not been called. */ @Nullable public X509Certificate getRootCA() { diff --git a/src/main/keystore/generate_self_signed_cert.sh b/src/main/keystore/generate_self_signed_cert.sh new file mode 100755 index 000000000..9754a5e9b --- /dev/null +++ b/src/main/keystore/generate_self_signed_cert.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/../.." + +tmpDir=$(mktemp -d) +trap 'rm -rf $tmpDir' EXIT + +cat > "$tmpDir/req.cnf" <7o&XvAMuO}6H!FXosJx-t39!ku;wUafwyrZDWq{y z!~W9-_F$`@w3eA6T&)5tI+k&&lKtwPeGNj$Xrgx_S#6Vps))COKAun5?b4i%4p}#` zU5lEw%jM#$OvY5awK*HY<8SDs#q{U(A?frpioPIGM68L8z# z$bF(K(0qX7P6k9rixj8sct?_pU-pQKo`1>l$Ui^2B&)@UZIP&+_gG_-fqWgA-xG8s zP{|d?|Nf{c?!Mc%+jGal3RlZzJGL1-!f+WJ1R*N=W0oV?mN{w&F`^M2%Xn9TWf%?C zEf*ZlTVV+ytzo&pdQ(q@fBjNcIhPa6&z~C1lewx+z4ytp3=-eMWHGFU4WWlA;2otH zhVI2>m!4p35%&Vf!!_XEu~-a`)*UI@k60g ziHWfEiSH*rMv2R*SH8$W&8bzPhNdmDdyVLbuW93Xgj^n8xck^T@KZn~o=*aPIZ-|> zy-!nLP;i`&%yYhBZz#!+)j~QE@Awg&bcKvXVL)x?kQ_@+;uXrn7^La6wBwJLt~qwO z)DT)nvogaq5k?Dxl7(!lwfB1-V)4w(aI@&j-R|ql?iHo14+GQJ8nk@uJOeqCNXCq< zU#;gTO*(x))^qyv%?^w1KT|jQR?fF-R-^)BD0!?5CFuM($UZ9udnHmHb2 z1c^oQ>_7AqQL+?m{KWsnWd*jBx9J1w6LrDeVF@(np(w;ql{s-gN(ewwCn8E?>WKN; zxekqvye<%jw>QH>3`{&?JBcmZ#Nu;a&|+po%=6KLFFsdvgHw9V>JoQ< z8g(kFh9S&7`kjHnPd*E08tW|^-XST!rWu`qn!*T$Fq$bUt zVpA`AUC;0E9pQy1fYX0->L2idV?YVu$TR!a+1;RH{67*>P=h%9&6oZgbi=dVP_^0R&$(_{{Hs?iCP4|H*JZXj#TqVktoHVG6^LAY8rK0r z$`~uGHpl*OKFgvjhXK1As!~8=H%|Vj@_hj*W{J}4YCo{){mxCwAV710bB;PWPARO8S`vpr7|H=4rCUcBjHpHDs8R$kU>nZ7a{KM9ojljH?9K^b}j6 z;e2vEbU(v2U0)u5qY9r+=L0hW{Szu+WRfJ>QhEwKxJ`|iL2W-8b&l%2<+bB=Ut+T6 zE4?D`)8RhQ~HdB|k7c1MmbLroZ044Sy>9i9fv(sSM4WD2WvuIv*s z=^528UbzokDC4S;k1*2WiQWhX-^N~xLPIxVRQLUm1Sa8E31Lh+Ji6I>mn zPaoMU_$xSi$$Gqa^!W=}tIe@mMY(2a!zoTd1*For+C#?J@^Q~U|C|@)iwg6TWu_yt z3sTCp{N{B@VuUCxR9rPqf$oJvEa1gFS3|AB-nA4}Js5W5NpMIZG^F2TL5L}!@#g8A zn^(4I5ZPt%$g@_dVI zUsgy-c0)E8p0|vP0q80}Xx-alB+9)eM87$;$ew5*sMTSUV-?>Wn~oY?gZz1zxoidY z-p%?@^hxV43xtC94A~9wWeZw>xxRnnI7SN9N$^!`|43mJTihzTUa{DvhU?Yy^;zvs z3#7y}B&D}3v>v{C?0Q3p^u0n?1xcM7YvW;KL_qk=6^8tpB4jl$QF#jo#qK+mT%6-F z9sssVFj=cd9(c1fPps2T?`?{PE8#<1k1tf;N)Rltx5C+qTLiT^K&*&Z|Iulx zdb32y%EejrB*W?F3s#C=lH z!3cZ+v(Hn*eU1)31FW<=8GYh(4y{&|2%r06wDlK|B5dO|4`$ZsJIqj<5~l^{-i*_m zt^-e8WVB1a)y;7J;L#8r+qd^8s1MRpk2)`x48hnwsMxw9%~!xJ@cTJmKmq~Khu z2r+Mu@^Rwj^_?;J2Jg}!qqs%ZS(6vxv5sZ)##u$?xg4uvMn7h&BME{hUTJz|@GqUW z(J?d)hAy&~b%SB4lbt~bNx1lL&_cKn+#pVTc01CSifYOhchDv9!X)C$(}{3>I06p) zJp%!>U>cCf!=1aI7hcG#M2}E6#n*aa+ literal 0 HcmV?d00001 diff --git a/src/src/main/resources/https/test-cert.pem b/src/src/main/resources/https/test-cert.pem new file mode 100644 index 000000000..abf4f0ff9 --- /dev/null +++ b/src/src/main/resources/https/test-cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID9TCCAt2gAwIBAgIUKgbdov3IOHFFX1ILK9nG6FE94oUwDQYJKoZIhvcNAQEL +BQAwgYoxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlv +cmsxEDAOBgNVBAoMB0plbmtpbnMxDTALBgNVBAsMBFRlc3QxFzAVBgNVBAMMDlNl +bGYtU2lnbmVkIENBMSEwHwYJKoZIhvcNAQkBFhJub3JlcGx5QGplbmtpbnMuaW8w +IBcNMjQxMDEwMTIwNDQ0WhgPMjEyNDA5MTYxMjA0NDRaMIGKMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRAwDgYDVQQKDAdKZW5r +aW5zMQ0wCwYDVQQLDARUZXN0MRcwFQYDVQQDDA5TZWxmLVNpZ25lZCBDQTEhMB8G +CSqGSIb3DQEJARYSbm9yZXBseUBqZW5raW5zLmlvMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA8nHUtTQRhvooEAkfUowZVMbXhNKPpTA2pAoR3Gq3wt2M +kDha7n43qfBDcGAT7MZ9RtSnyErrVGzRnvhU++VUwhaCZHoc0CZ7IYL9MXAdzUOS +CXzzEXpUXXJRYBHG6lXPBXWpKHe+ds561bGr3j8rgJGrA1Vqwp284FXDzSR4rphT +q6jjCYh1MQkmA7QZMItHsSL4BLekopH7IVmv6f7WCnjZMX1c8awuHcwWbf4k4ujM +uJdvTCr8SO/y8MuYuwk2esa4qGBSoStE1dX8hktjWJyLw+m+5fxofmywBdTWQ6ju +ci0u3bJC2mlYVxbh4KnuZojlb8BpXA9Ki7YiiSqruwIDAQABo08wTTALBgNVHQ8E +BAMCB4AwCQYDVR0TBAIwADAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYE +FF20ENS6djGGyP6otWNk/C7dTGO/MA0GCSqGSIb3DQEBCwUAA4IBAQDSVh+fD/o6 +8rr4L7HupIqxod+jzqP9IOYgXPgEa3e9GRcgMPFkUMV9Nzfy9nU4dVO330axO8gc +041m2drTM+hajHOuCMlJCEkPCWnFqxo6Xd1SqdOjxLRudfcysiUi7vYph5MTPzgD +Bci3u/LT9LcKsAPb0Mz6qNEC/XPPyzE+9q+YCM9l2wiiNUkTAmVPW1hKxL/yrsrq ++DqUUMeSNtIZRaIRbSOPjq64wPtBUVZKoXy3MVPkwNL2hQvL0KUg6Nx2N2xM9eWK +c80BT7yK8U79KXf5HtZNkmd+SaaV70smYDb7H08VBIGjZjUil83rJIhrYEkdJ4xn +fqkPI9hkUIQK +-----END CERTIFICATE----- diff --git a/src/src/main/resources/https/test-keystore.p12 b/src/src/main/resources/https/test-keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..f58179327dc9edc02d7c58c0da1e9e9102903fcc GIT binary patch literal 2787 zcmai$X*3iH8^>qH%-9WM$-b1aFJs?{Y}vOYlzo{Z##l0z7?~*9NwP1|2t(PI5XO?S zTuLTcODJNz`!_Nmg~81AmJKseTQaUg zASlg_S;R{ahz5XUfYSW$Du@;WK=MIpUGc_1FDful0?eFhJ;jx@oa=5^Dl28_Hpmx) z1G^Tgb40J|G-NJm_hrY~zLA#^GA^=bC)~*$l`wC+=HWE1c`n*X>Lr?QV88rUc}nz> zo}zD=X8&DCkhGI9w7%n!BbJ~5_^zDX_#?HtnJ(&uMppm@>oM0U^d#f+8kC-a$|&gS z7&B3uHyLWmeGw|{8c*c!{E5nt3 zZZHLJ0%s%@;Ma60Fm!;OpXld0Xv3e$G4zfrX=OWG#K$dk=R83!!*82GwfEuDU4az- zung}}uuAGs!geVAjc<-2o#D%`C2i|CDSnw1dT}|T*<~&_vcoek-Kegh9oJsIrjhx| zE;Pr%?p`aELTX1(yphmwk9SJhoz$INDv(voG9tf8hO9z+lV#A$j7aRXO#S z)3>N}a$q92&hlI!SSK3*QrbsGX_ZJ`aGo#R4q8L%wwDyWJv9!tn_M2=d=W9jVn`C#-#}P=W*JwnDvdMt6XWEVxF`E>W$E9TjPEDnH=( zPzl~hsF)h(Auy|lQ*cQv4Ok|NXH%+N>o0=Mlza=|X5#e+GPXI|T1|^1Fn?v$1l_Rq zAJv$3jy;tKq3nBzsJ0dPoq~<5*2!SnX`L*f*hDDPbE6U&KzrUIp`bM;i_FL}|Ipv- z_K2B=Nxoj=REf4duccMtn;uxtk8Jz2qtI+V|Gk#EJKyXG)mDf@@Z$;_XVgMoJ?<3s zZbbC=E^*eXpq|GZ{Pwci?2t1Q?M8K3Zbft5y-3O_hKY$P!(GUy^Z|!ujPlm6zm)yI zlg`a-K5{C;i-r-XSmau+W3K1MQ``MKvH``+qM3qNW2V+6n;1mkxvJ7Dk?H+UHkt{mn&N|EPT8NThPt^qm5~6cKvajjzb-5)uqd;42PTz8cP3v zM>5a>L1(4f8C3c&@!i}h0nxV!ma*U2^25j{`u?Z*YK*yElo3zJ`h6z%t#NR7@R?`< zb4WyfRx18gdEnjf>zhi0`nL6m@WKg497!lZa7l$nOD00Ji*0SEX_nuBR=9EBL(bA0 z?o)GYzd&*#uNSPuWGiu1Dc1&-WrOURMDm^6wmx_~yy)}Uf&%Q6W+(ipHo#2lXTB~E z3vN<$&2XMfxsR!BON~nlqYYPj-DlK1Rxl#8(Avv85Zk`R&EMXi=R@tO{Yl`mJGqLa z>5qQouposb;SDT%ob?nC(T)v z#-EE^fm%JV4cMdro+KsQ_los^ zR5$TZ`#!phafC$i(W@{S@tf-!1$^!_pX)Mdi>H0jU09TSpPiGrNM{3aaP!&rM@d%U z8kukOc3?rtWPgTu9I<+h@j?2qr}kEJc^$j#k*+QjQ7 zGps7jYz^?5yzNCUC{q&zu$HYi$ynr{&l@h;mVbb`CtI9AVdGht-$^)!gWYy4-V>;3 zBX%2`j+Kq{782(wXEH6lj9&3Vn$Q2hJqkswn(&NCl+)cawz+j}u_`6yF}(?EkvJWF zVtI`7VjZ=98kWUF{j?W%*ZDYQDWP^-xX?e$*}_9;Wy?nO8pTSL%1c#`r&vEJCC3Fb zj~D#vYLTDw)uxwWyZC7Gvi0oj?754>W+|o4cE)=|s}IS+r1vnL2!f7KiwLhhAWTY% zJ1Hf8CnBQi$WuMS*~AH6t)PS5#n9`UcBiRgqDS99<*6T0vzJ;}y7VgLVivjr%QtlT zfK0iwii_hswwqJqWA3sL%9QS~X8IXaH8yDJCZd9E7>VR}Ld?oFqaF-aNCFgd1w;;1h$agyG3gM)1Hl zIs1$KGXca=$G|$$s+#RBPr;5F*7UXN_1lqVj)G{V6 zg$ z$gNJ!$XPVCQTb_j$H1)3bwjUsz#!87uj+poC{#p{H@FXMI2%8m66V z4@>N|`byoPlGf3^d!<9g;G_*Ad+|BaH=AY+GI7(hG5w_^)?bW4-)j}~N19B4%v!BlI~b7nHpWv+xM{$X zWIX3uSd-J?l9l^DFH#pNhh+To7J`5f09erc=0O{&cOiUT-5X)}z4$9hNzC{dSL01H Yxa#~NPLc;q<0~LyK3dCu?H|?t4ejdxXaE2J literal 0 HcmV?d00001