diff --git a/src/main/java/de/mlessmann/certassist/except/JavaSecurityException.java b/src/main/java/de/mlessmann/certassist/except/JavaSecurityException.java new file mode 100644 index 0000000..4325fae --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/except/JavaSecurityException.java @@ -0,0 +1,20 @@ +package de.mlessmann.certassist.except; + +public class JavaSecurityException extends Exception { + + public JavaSecurityException() { + super(); + } + + public JavaSecurityException(String message) { + super(message); + } + + public JavaSecurityException(String message, Throwable cause) { + super(message, cause); + } + + public JavaSecurityException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/de/mlessmann/certassist/keystore/KeyStoreManager.java b/src/main/java/de/mlessmann/certassist/keystore/KeyStoreManager.java new file mode 100644 index 0000000..3023753 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/keystore/KeyStoreManager.java @@ -0,0 +1,90 @@ +package de.mlessmann.certassist.keystore; + +import de.mlessmann.certassist.except.JavaSecurityException; +import de.mlessmann.certassist.openssl.CertificatePasswordProvider; +import de.mlessmann.certassist.openssl.CertificateUsage; +import de.mlessmann.certassist.openssl.OpenSSLCertificateCreator; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class KeyStoreManager { + + private static final OpenOption[] CREATE_TRUNCATE = { + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + }; + private final OpenSSLCertificateCreator certificateCreator; + private final CertificatePasswordProvider passwordProvider; + + public KeystoreUsage createKeyStore(String keyStorePassphrase, CertificateUsage... serverCerts) + throws JavaSecurityException { + try { + Path keystorePath = Files.createTempFile("keystore", ".jks"); + String keystorePassword = "changeit"; + String alias = "mykey"; + + // Load the keystore + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(null, null); + + for (CertificateUsage serverCert : serverCerts) { + PrivateKey privateKey = loadPrivateKey( + serverCert.certificateKeyPath(), + passwordProvider.getPasswordFor(serverCert.fingerprint()) + ); + Certificate[] certChain = loadCertificateChain(serverCert.fullchainPath()); + keystore.setKeyEntry(alias, privateKey, keystorePassword.toCharArray(), certChain); + } + + // Save the keystore + try (var keystoreOut = Files.newOutputStream(keystorePath, CREATE_TRUNCATE)) { + keystore.store(keystoreOut, keystorePassword.toCharArray()); + } + return new KeystoreResult(keystorePath); + } catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException e) { + throw new JavaSecurityException("Failed to create keystore!", e); + } + } + + @SneakyThrows + private PrivateKey loadPrivateKey(Path privateKey, String passphrase) { + String pemContent; + if (certificateCreator.isKeyEncrypted(privateKey)) { + pemContent = certificateCreator.readDecryptedKey(privateKey, passphrase); + } else { + pemContent = Files.readString(privateKey); + } + + try (var fis = Files.newInputStream(privateKey)) { + String privateKeyPEM = pemContent + .replaceAll(".*?-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s", ""); + byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM); + return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decodedKey)); + } + } + + @SneakyThrows + private Certificate[] loadCertificateChain(Path certChainPath) { + // Load the certificate chain from a PEM file + try (var fis = Files.newInputStream(certChainPath)) { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return certFactory.generateCertificates(fis).toArray(Certificate[]::new); + } + } +} diff --git a/src/main/java/de/mlessmann/certassist/keystore/KeystoreResult.java b/src/main/java/de/mlessmann/certassist/keystore/KeystoreResult.java new file mode 100644 index 0000000..2e39746 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/keystore/KeystoreResult.java @@ -0,0 +1,19 @@ +package de.mlessmann.certassist.keystore; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +record KeystoreResult(Path truststorePath) implements KeystoreUsage { + @Override + public void close() { + try { + Files.deleteIfExists(truststorePath); + } catch (IOException e) { + log.warn("Failed to delete truststore at {}. Scheduling delete on exit.", truststorePath, e); + truststorePath.toFile().deleteOnExit(); + } + } +} diff --git a/src/main/java/de/mlessmann/certassist/keystore/KeystoreUsage.java b/src/main/java/de/mlessmann/certassist/keystore/KeystoreUsage.java new file mode 100644 index 0000000..baee156 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/keystore/KeystoreUsage.java @@ -0,0 +1,26 @@ +package de.mlessmann.certassist.keystore; + +import de.mlessmann.certassist.except.JavaSecurityException; +import java.io.IOException; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public interface KeystoreUsage extends AutoCloseable { + Path truststorePath(); + + @Override + default void close() { + // Default implementation does nothing - overwrite this if you need to close resources. + } + + default KeyStore readAsKeystore(String passphrase) throws JavaSecurityException { + try { + return KeyStore.getInstance(truststorePath().toFile(), passphrase.toCharArray()); + } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { + throw new JavaSecurityException(e); + } + } +} diff --git a/src/main/java/de/mlessmann/certassist/keystore/TruststoreManager.java b/src/main/java/de/mlessmann/certassist/keystore/TruststoreManager.java new file mode 100644 index 0000000..131ecbd --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/keystore/TruststoreManager.java @@ -0,0 +1,62 @@ +package de.mlessmann.certassist.keystore; + +import de.mlessmann.certassist.openssl.CertificateUsage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class TruststoreManager { + + private static final OpenOption[] CREATE_TRUNCATE = { + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + }; + + public KeystoreUsage createTruststore(String truststorePassphrase, CertificateUsage... trustedCertificates) { + try { + Path truststorePath = Files.createTempFile("truststore", ".jks"); + truststorePath.toFile().deleteOnExit(); + log.debug("Creating truststore at {}", truststorePath); + + KeyStore truststore = buildTruststore(trustedCertificates); + try (var outputStream = Files.newOutputStream(truststorePath, CREATE_TRUNCATE)) { + truststore.store(outputStream, truststorePassphrase.toCharArray()); + } + return new KeystoreResult(truststorePath); + } catch (IOException | KeyStoreException | CertificateException | NoSuchAlgorithmException e) { + throw new IllegalStateException("Failed to create truststore!", e); + } + } + + @SneakyThrows + private static KeyStore buildTruststore(CertificateUsage[] trustedCertificates) { + KeyStore truststore = KeyStore.getInstance(KeyStore.getDefaultType()); + truststore.load(null, null); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + if (trustedCertificates == null || trustedCertificates.length == 0) { + log.warn("No trusted certificates provided, truststore will be empty!"); + } else { + for (CertificateUsage trustedCertificate : trustedCertificates) { + try (var inputStream = Files.newInputStream(trustedCertificate.certificatePath())) { + X509Certificate jdkCert = (X509Certificate) certificateFactory.generateCertificate(inputStream); + truststore.setCertificateEntry(trustedCertificate.fingerprint(), jdkCert); + } + } + } + return truststore; + } +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java index 1e7e394..1974fd1 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java @@ -36,5 +36,7 @@ public interface CertificateUsage extends AutoCloseable { String fingerprint(); @Override - void close(); + default void close() { + // Default implementation does nothing - overwrite this if you need to close resources. + } } diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index add5193..8300293 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -139,8 +139,9 @@ public class OpenSSLCertificateCreator { Path certAuthFullchain = Optional .ofNullable(certAuthority.fullchainPath()) .orElse(certAuthority.certificatePath()); - Files.write(fullchain, Files.readAllBytes(certAuthFullchain), StandardOpenOption.CREATE); + // Leaf certificate first, then the CA chain Files.write(fullchain, Files.readAllBytes(signedCert), StandardOpenOption.APPEND); + Files.write(fullchain, Files.readAllBytes(certAuthFullchain), StandardOpenOption.CREATE); } catch (IOException e) { throw new CommandLineOperationException("Failed to create fullchain file.", e); } @@ -616,4 +617,30 @@ public class OpenSSLCertificateCreator { default -> throw new IllegalStateException("Unexpected subject key: %s in line: %s".formatted(key, line)); }; } + + public String readDecryptedKey(Path keyFile, String passphrase) throws CommandLineOperationException { + StartedProcess keyReadProc = null; + try { + keyReadProc = + new ProcessExecutor() + .command(resolveOpenSSL(), "rsa", "-in", keyFile.toString(), "-passin", OSSL_ARG_KEY_PW) + .environment(OSSL_ENV_KEY_PW, passphrase) + .readOutput(true) + .redirectError(Slf4jStream.of(openSSLLogger).asError()) + .start(); + var keyReadResult = keyReadProc.getFuture().get(30, SECONDS); + if (keyReadResult.getExitValue() != 0) { + throw new CommandLineOperationException( + "Failed to read decrypted key - is the passphrase correct? Exit code: %d".formatted( + keyReadResult.getExitValue() + ) + ); + } + return keyReadResult.getOutput().getUTF8(); + } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } finally { + killIfActive(keyReadProc); + } + } } diff --git a/src/test/java/de/mlessmann/certassist/TestKeystoreCreation.java b/src/test/java/de/mlessmann/certassist/TestKeystoreCreation.java new file mode 100644 index 0000000..71037ff --- /dev/null +++ b/src/test/java/de/mlessmann/certassist/TestKeystoreCreation.java @@ -0,0 +1,119 @@ +package de.mlessmann.certassist; + +import static org.assertj.core.api.Assertions.assertThat; + +import de.mlessmann.certassist.keystore.KeyStoreManager; +import de.mlessmann.certassist.keystore.TruststoreManager; +import de.mlessmann.certassist.openssl.CertificateProvider; +import de.mlessmann.certassist.openssl.CertificateUsage; +import de.mlessmann.certassist.openssl.OpenSSLCertificateCreator; +import de.mlessmann.certassist.service.InMemoryCertificatePasswordProvider; +import java.io.IOException; +import java.net.ServerSocket; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class TestKeystoreCreation { + + private static final String STORE_PASSPHRASE = "changeit"; + private static final SecureRandom TEST_RANDOM = new SecureRandom(); + private final CertificateUsage dummyCert = new CertificateUsage() { + @Override + public String fingerprint() { + return TestOpenSSLCertificateCreator.TEST_CERT_FINGERPRINT; + } + + @Override + public Path certificatePath() { + return TestOpenSSLCertificateCreator.TEST_CERT_PATH.resolve("x509forImport.pem"); + } + + @Override + public Path certificateKeyPath() { + return TestOpenSSLCertificateCreator.TEST_CERT_PATH.resolve("x509forImport.key.pem"); + } + + @Override + public Path fullchainPath() { + return TestOpenSSLCertificateCreator.TEST_CERT_PATH.resolve("x509forImport.fullchain.pem"); + } + }; + + @Test + void testTruststore() throws Exception { + var passwordProvider = new InMemoryCertificatePasswordProvider(); + passwordProvider.setPasswordFor(dummyCert.fingerprint(), TestOpenSSLCertificateCreator.TEST_CERT_PASSPHRASE); + + var certificateProvider = Mockito.mock(CertificateProvider.class); + var opensslCertCreator = new OpenSSLCertificateCreator( + new ExecutableResolver(), + passwordProvider, + certificateProvider + ); + var truststoreManager = new TruststoreManager(); + var keyStoreManager = new KeyStoreManager(opensslCertCreator, passwordProvider); + AtomicBoolean serverAccepted = new AtomicBoolean(false); + AtomicBoolean clientAccepted = new AtomicBoolean(false); + + try ( + var tmpTruststore = truststoreManager.createTruststore(STORE_PASSPHRASE, dummyCert); + var tmpKeyStore = keyStoreManager.createKeyStore(STORE_PASSPHRASE, dummyCert) + ) { + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm() + ); + keyManagerFactory.init(tmpTruststore.readAsKeystore(STORE_PASSPHRASE), STORE_PASSPHRASE.toCharArray()); + + SSLContext tlsSrvContext = SSLContext.getInstance("TLS"); + tlsSrvContext.init(keyManagerFactory.getKeyManagers(), null, TEST_RANDOM); + int serverPort = 1024 + TEST_RANDOM.nextInt(22_000); + + ServerSocket serverSocket = tlsSrvContext.getServerSocketFactory().createServerSocket(0); + var serverThread = Thread.startVirtualThread(() -> { + try { + var remote = serverSocket.accept(); + serverAccepted.set(true); + try { + Thread.sleep(2_000); + } catch (InterruptedException e) { + // nothing + } + remote.close(); + } catch (IOException e) { + throw new IllegalStateException("Failed to create server socket!", e); + } + }); + + var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(tmpTruststore.readAsKeystore(STORE_PASSPHRASE)); + + SSLContext tlsContext = SSLContext.getInstance("TLS"); + tlsContext.init(null, trustManagerFactory.getTrustManagers(), TEST_RANDOM); + + var clientThread = Thread.startVirtualThread(() -> { + try { + var socket = tlsContext.getSocketFactory().createSocket("127.0.0.1", serverSocket.getLocalPort()); + clientAccepted.set(true); + socket.close(); + } catch (IOException e) { + throw new IllegalStateException("Failed to create client socket!", e); + } + }); + + serverThread.join(); + clientThread.join(); + if (!serverSocket.isClosed()) { + serverSocket.close(); + } + + assertThat(serverAccepted.get()).withFailMessage("Server did not accept connection!").isTrue(); + assertThat(clientAccepted.get()).withFailMessage("Client did not accept connection!").isTrue(); + } + } +} diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index c39a807..456da58 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -9,7 +9,6 @@ import java.nio.file.Path; import java.util.Objects; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -18,18 +17,16 @@ class TestOpenSSLCertificateCreator { public static final String TEST_CERT_PASSPHRASE = "ABC-123"; public static final Path TEST_CERT_PATH = Path.of("src/test/resources/openssl"); - public static final String ERR_NOT_ENCRYPTED = "Private key not encrypted"; - public static final String ERR_VERIFY_FAILED = "Certificate verification failed"; - - @Autowired - OpenSSLCertificateCreator openSSLCertificateCreator; + public static final String TEST_CERT_FINGERPRINT = + "SHA1;4E:D6:0A:47:F0:63:AD:96:26:83:16:28:32:F5:E8:36:5A:62:91:95"; + private static final String ERR_NOT_ENCRYPTED = "Private key not encrypted"; + private static final String ERR_VERIFY_FAILED = "Certificate verification failed"; @MockBean CertificatePasswordProvider passwordProvider; @BeforeEach void setUp() { - passwordProvider = mock(CertificatePasswordProvider.class); when(passwordProvider.generateNewPassword()).thenReturn(TEST_CERT_PASSPHRASE); when(passwordProvider.getPasswordFor(anyString())).thenReturn(TEST_CERT_PASSPHRASE); } diff --git a/src/test/resources/openssl/x509forImport.fullchain.pem b/src/test/resources/openssl/x509forImport.fullchain.pem index 7dce998..b5fb6f7 100644 --- a/src/test/resources/openssl/x509forImport.fullchain.pem +++ b/src/test/resources/openssl/x509forImport.fullchain.pem @@ -1,36 +1,4 @@ -----BEGIN CERTIFICATE----- -MIIFgTCCA2mgAwIBAgIUVTm2kFBiacDG3Om6JFaZKvJ6CScwDQYJKoZIhvcNAQEL -BQAwUDELMAkGA1UEBhMCREUxCzAJBgNVBAgMAlNIMQswCQYDVQQHDAJISDETMBEG -A1UECgwKQ3JhenktQ2F0czESMBAGA1UEAwwJdGVzdC5ob21lMB4XDTI0MTEyMjE4 -NTc0MFoXDTI1MTEyMjE4NTc0MFowUDELMAkGA1UEBhMCREUxCzAJBgNVBAgMAlNI -MQswCQYDVQQHDAJISDETMBEGA1UECgwKQ3JhenktQ2F0czESMBAGA1UEAwwJdGVz -dC5ob21lMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnnYaTE8TKKTu -IE2hcHwHm0RnM+4VwPnNT6q6b4oYSJJeGJCbQYt8CAAkBxvY3j1H0xP6imD7ULUK -ymx2fQiHK+bRAdXyoguLHaPYWPInfUyHp9Y2w2AglKCG/U2paMni4xL0LH4N3V1r -x5hQG9ORwJzVH5wMiYoETzgbd1ED7G0tVuKYrH84Ma8znEXVZ4XfAlDfYEGKPNDN -dZDkFEQFYHb5RSPB6ym3vrbZJfLkNy01m/Cpdj3/GqJ460zo7x/apzVPNj/khW1v -fME8c8sz1LqEbQBVdUU9xN8DfTjT/z1NHA5S/1O7Yb9Z0t1tjvs3u/iyCjBNMbd5 -7FNqMVjXFjENav21zrrr0LAiUfBcbokQZD72/j053gXz9LkcTjLvkPN3i1oVfNIA -b/Ce4hHzpWk2kvZuFyqfKj8Yc8oTfjr88sGxe1JCgbhnCvkw/5d7hy7OJVG4mTIH -WliOY92R7xPwEBhqc+A4VYqhAIrp+Qzen+XGrBih+PneEsSWVfM3PyjKt/qURK4j -gWCGFIEI80xyFaHhwHpeKszZcMOVAHjV3Ik91wutL/HoZ0r5uPYvwqQb6QZ7ubqX -FWiuNUf970TFS7eIA33Xr0IojPBziinU3/uYnJBODR4Sl2npijzwqsRrGsrSDUF7 -cxxhsyv64ri2p3u4wnP4faLf1Dtk3kUCAwEAAaNTMFEwHQYDVR0OBBYEFICs29k3 -J5Pnza2xgkgycaxBxAcSMB8GA1UdIwQYMBaAFICs29k3J5Pnza2xgkgycaxBxAcS -MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAFiqNg1yXlajI1ZJ -l4qXADvi1xpFLEn17162jN+zb6DgRRohqwrsP71fAqyuCticYELk3gBHt8KizEBj -xPSggahX+ZIGG5/Tnb5cQ61y0GMZHWyABHjgiOGi26Gxar5wi7ET6W4D6w09u8U3 -wd8q8U164aSj6Rh153S0r8SJJOhqtiZhcYllOMB8SNgjxodlco+YvCGoqkhPdJef -H2S02mgbjxMlo8P4ivoirD7boLirlXkoNidmaWvC/hD4ZwsTWMrRnHL1S5DEhKef -WSgtX3tUEvHrEyqsGcSn49l4CNE6Xbx5wWo+4c9bs642f7u34OoUnitYmGZDzT1d -zQOyBirn9k6fB/SX7Ug+PF6+KbJCXEdWffgNqe5HAG8Lo2EOEnocp3kncRMSq3zY -qzbTBqaWI512cqN2RA+Q0NPzVH01jPG8yKaSxVzf4Sp1Iqjl7fe9WZL9/L4DJZ7o -QWRPZvwH5Rz8utGUylfe9LxSisX7xZMNoGLqsQfanowqZCiS1M1GRoHsILnFuFNw -3ONu4qp0Gr3+PxsKoE6NBstqD6Lkrm0uf/IhC7fwIbdq3qqe0E4xZou9W9nkmapw -0iKhdhCtnp3HsF6f2Kc0ipyPxVXDnP5TCx4NLst2RvoYryUBwqIKfwU8X3yOBmhq -HowJo/ZfrlzZf8753cfF8Kza6NUA ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIFrjCCA5agAwIBAgIUMhyzffHXKaQnfX/ckbx3KZ9AVXswDQYJKoZIhvcNAQEL BQAwUDELMAkGA1UEBhMCREUxCzAJBgNVBAgMAlNIMQswCQYDVQQHDAJISDETMBEG A1UECgwKQ3JhenktQ2F0czESMBAGA1UEAwwJdGVzdC5ob21lMB4XDTI0MTEyMjE4 @@ -63,3 +31,35 @@ USrygPv8w4UT3SFoiABp1ThDjCheROLISAo/SIag8XUw58bmmiDRuVPLZDQfYBYu KgSLJpFB99QMzvhkNBhImEpmWRrKAFSmwNG4/l/yR19udd4AxpjH62LT8mUZOf23 dQ6pRi1hNlM1v6ZvFDdq8Rgw -----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgTCCA2mgAwIBAgIUVTm2kFBiacDG3Om6JFaZKvJ6CScwDQYJKoZIhvcNAQEL +BQAwUDELMAkGA1UEBhMCREUxCzAJBgNVBAgMAlNIMQswCQYDVQQHDAJISDETMBEG +A1UECgwKQ3JhenktQ2F0czESMBAGA1UEAwwJdGVzdC5ob21lMB4XDTI0MTEyMjE4 +NTc0MFoXDTI1MTEyMjE4NTc0MFowUDELMAkGA1UEBhMCREUxCzAJBgNVBAgMAlNI +MQswCQYDVQQHDAJISDETMBEGA1UECgwKQ3JhenktQ2F0czESMBAGA1UEAwwJdGVz +dC5ob21lMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnnYaTE8TKKTu +IE2hcHwHm0RnM+4VwPnNT6q6b4oYSJJeGJCbQYt8CAAkBxvY3j1H0xP6imD7ULUK +ymx2fQiHK+bRAdXyoguLHaPYWPInfUyHp9Y2w2AglKCG/U2paMni4xL0LH4N3V1r +x5hQG9ORwJzVH5wMiYoETzgbd1ED7G0tVuKYrH84Ma8znEXVZ4XfAlDfYEGKPNDN +dZDkFEQFYHb5RSPB6ym3vrbZJfLkNy01m/Cpdj3/GqJ460zo7x/apzVPNj/khW1v +fME8c8sz1LqEbQBVdUU9xN8DfTjT/z1NHA5S/1O7Yb9Z0t1tjvs3u/iyCjBNMbd5 +7FNqMVjXFjENav21zrrr0LAiUfBcbokQZD72/j053gXz9LkcTjLvkPN3i1oVfNIA +b/Ce4hHzpWk2kvZuFyqfKj8Yc8oTfjr88sGxe1JCgbhnCvkw/5d7hy7OJVG4mTIH +WliOY92R7xPwEBhqc+A4VYqhAIrp+Qzen+XGrBih+PneEsSWVfM3PyjKt/qURK4j +gWCGFIEI80xyFaHhwHpeKszZcMOVAHjV3Ik91wutL/HoZ0r5uPYvwqQb6QZ7ubqX +FWiuNUf970TFS7eIA33Xr0IojPBziinU3/uYnJBODR4Sl2npijzwqsRrGsrSDUF7 +cxxhsyv64ri2p3u4wnP4faLf1Dtk3kUCAwEAAaNTMFEwHQYDVR0OBBYEFICs29k3 +J5Pnza2xgkgycaxBxAcSMB8GA1UdIwQYMBaAFICs29k3J5Pnza2xgkgycaxBxAcS +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAFiqNg1yXlajI1ZJ +l4qXADvi1xpFLEn17162jN+zb6DgRRohqwrsP71fAqyuCticYELk3gBHt8KizEBj +xPSggahX+ZIGG5/Tnb5cQ61y0GMZHWyABHjgiOGi26Gxar5wi7ET6W4D6w09u8U3 +wd8q8U164aSj6Rh153S0r8SJJOhqtiZhcYllOMB8SNgjxodlco+YvCGoqkhPdJef +H2S02mgbjxMlo8P4ivoirD7boLirlXkoNidmaWvC/hD4ZwsTWMrRnHL1S5DEhKef +WSgtX3tUEvHrEyqsGcSn49l4CNE6Xbx5wWo+4c9bs642f7u34OoUnitYmGZDzT1d +zQOyBirn9k6fB/SX7Ug+PF6+KbJCXEdWffgNqe5HAG8Lo2EOEnocp3kncRMSq3zY +qzbTBqaWI512cqN2RA+Q0NPzVH01jPG8yKaSxVzf4Sp1Iqjl7fe9WZL9/L4DJZ7o +QWRPZvwH5Rz8utGUylfe9LxSisX7xZMNoGLqsQfanowqZCiS1M1GRoHsILnFuFNw +3ONu4qp0Gr3+PxsKoE6NBstqD6Lkrm0uf/IhC7fwIbdq3qqe0E4xZou9W9nkmapw +0iKhdhCtnp3HsF6f2Kc0ipyPxVXDnP5TCx4NLst2RvoYryUBwqIKfwU8X3yOBmhq +HowJo/ZfrlzZf8753cfF8Kza6NUA +-----END CERTIFICATE-----